Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
315a360
Remove some desynced test file pointers
asgersvenning Apr 4, 2025
b0d6974
Fixed error on saving with SVG due to invalid execution flow logic in…
asgersvenning Apr 7, 2025
6f9aee6
Fix --long_format inference
asgersvenning Apr 7, 2025
ab5361b
Stricter version requirement for ultralytics dependency due to a brea…
asgersvenning Jun 6, 2025
f59cec5
Change README install guide from git clone with SSH to HTTPS (re: #121)
asgersvenning Jun 6, 2025
72f407a
Remove tile and image tensors from intermediate `Results` objects to …
asgersvenning Jul 9, 2025
1addfac
Merge branch 'develop' of github.com:darsa-group/flat-bug into develop
qgeissmann Oct 6, 2025
8e23f7f
work on #134
qgeissmann Oct 8, 2025
22acf67
improved
qgeissmann Oct 12, 2025
135eb6d
Merge branch 'feature_mask_mask_refiner' of github.com:darsa-group/fl…
qgeissmann Oct 12, 2025
18f4044
Merge branch 'develop' of github.com:darsa-group/flat-bug into develop
qgeissmann Jan 29, 2026
1c26793
Merge branch 'develop' of github.com:darsa-group/flat-bug into develop
qgeissmann Feb 5, 2026
24f46e2
Merge branch 'develop' of github.com:darsa-group/flat-bug into develop
qgeissmann May 7, 2026
7605ab5
Merge branch 'develop' of github.com:darsa-group/flat-bug into develop
qgeissmann May 13, 2026
e5d4da1
Add YOLOv26 support for training and inference
qgeissmann May 13, 2026
a2e7c6d
Simplify Predictor model loading, fix ultralytics 8.4.x compatibility
qgeissmann May 13, 2026
75c1305
Name training runs as fb_{size}_{timestamp} and update N40S config
qgeissmann May 14, 2026
75e0d97
Default to YOLOv26 for training, fix model names in configs
qgeissmann May 18, 2026
3a818f6
Download pretrained weights explicitly when not present locally
qgeissmann May 18, 2026
e551822
Free GPU cache before validation to avoid OOM from fragmentation
qgeissmann May 19, 2026
b00fba5
Cap validation batch size at training batch size to avoid OOM
qgeissmann May 19, 2026
8a19b4b
Use batch size 1 for validation to avoid OOM
qgeissmann May 20, 2026
87e5692
Use PNG compress_level=1 for crop saving to reduce write time
qgeissmann May 20, 2026
463d7c6
ibid
qgeissmann Jun 4, 2026
9570493
Suppress validation loss to fix OOM with YOLOv26 one2many head
qgeissmann Jun 25, 2026
289f8b3
Fix tensor shape mismatch in validation loss no-op
qgeissmann Jun 26, 2026
ce66fab
Compatibility fixes and small modernization changes
asgersvenning Jul 7, 2026
6fe0637
Update lint to adhere more closely to `ruff format`
asgersvenning Jul 7, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ jobs:
python-version: ${{ matrix.python-version }}

- name: Install the project
run: uv sync --locked --all-extras --group tests
run: uv sync --locked --all-extras

- name: Lint with Ruff
run: uv run ruff check . --ignore E501
Expand Down
1 change: 0 additions & 1 deletion .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ jobs:
- name: Install dependencies
run: |
uv sync --all-extras --group docs
uv pip install sphinx sphinx_rtd_theme myst_parser furo
- name: Sphinx build
run: |
cd ./docs
Expand Down
219 changes: 219 additions & 0 deletions prototypes/mask_refiner/fb_refine.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
import json
import argparse
import os

import cv2
import cv2 as cv
import numpy as np
from torchgen.gen_functionalization_type import return_from_mutable_noop_redispatch


# fixme, resume should continue on the same "run folder"
def main():
args_parse = argparse.ArgumentParser(formatter_class=argparse.RawTextHelpFormatter)
args_parse.add_argument("-d", "--data-dir", dest="data_dir",
help="The directory containing the prepared data (i.e., the output of `fb_prepare.py`",
type=str)

args_parse.add_argument("-c", "--config-file", dest="config_file",
help="A YAML-formatted config file that overrides the default training meta-parameters",
default=None)
args_parse.add_argument("-r", "--resume", dest="resume",
help="resume training",
action='store_true')

args, extra = args_parse.parse_known_args()

# def best_new_contour(cnt):



def _iou_from_masks(a, b):
inter = np.logical_and(a, b).sum()
union = np.logical_or(a, b).sum()
return float(inter) / float(union) if union > 0 else 0.0

def yolo_ensemble_contour(image_bgr, cnt, conf_threshold=0.25, iou_threshold=0.1, mask_threshold=1):


# get global YOLOv8 model

H, W = image_bgr.shape[:2]
accum = np.zeros((H, W), dtype=np.float32)
original_mask = np.zeros_like(accum).astype(np.uint8)
cv.fillPoly(original_mask, [cnt.astype(np.int32)], 1)
n=4
for i in range(n):
print(i)
if i ==0:
image_in = np.copy(image_bgr)
elif i == 1:
image_in = cv.rotate(image_bgr, cv.ROTATE_90_CLOCKWISE)
elif i == 2:
image_in = cv.flip(image_bgr, 0)
elif i == 3:
image_in = cv.flip(cv.rotate(image_bgr, cv.ROTATE_90_CLOCKWISE), 0)
else:
raise ValueError("aug_idx must be in {0,1,2,3}")

# image_in =cv.medianBlur(image_bgr,i*2+1)
res = yolo(image_in, verbose=False)[0]

# no masks predicted this run
if not hasattr(res, "masks") or res.masks is None or getattr(res.masks, "data", None) is None:
continue

confs = res.boxes.conf.detach().cpu().numpy()
masks_t = res.masks.data # torch.Tensor [num, h, w]
num_masks = masks_t.shape[0]

valid_masks = []
mask_areas = []

for j in range(num_masks):
if confs is not None and j < len(confs) and confs[j] < conf_threshold:
continue

m = masks_t[j].detach().cpu().numpy().astype(np.float32) # float mask (h, w) in [0,1]

iou = _iou_from_masks(m, original_mask)
print(j, iou)
if iou < iou_threshold :
continue
valid_masks.append(m)
mask_areas.append(np.sum(m))

if len(mask_areas) == 0:
continue

m = valid_masks[np.argmax(mask_areas)]

if i == 0:
m = m
elif i == 1:
m = cv.rotate(m, cv.ROTATE_90_COUNTERCLOCKWISE)
elif i == 2:
m = cv.flip(m, 0)
elif i == 3:
m = cv.rotate(cv.flip(m,0), cv.ROTATE_90_COUNTERCLOCKWISE)
else:
raise ValueError("aug_idx must be in {0,1,2,3}")
print (i,m.shape)
accum += m

frac = accum / float(n)
cv2.imshow("test2", frac)
final_mask = (frac >= mask_threshold).astype(np.uint8) * 255
if np.count_nonzero(final_mask) == 0:
return cnt
# find largest contour
cnts, _ = cv.findContours(final_mask, cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE)
if not cnts:
return None

best = max(cnts, key=cv.contourArea) # largest by area
best = cv.approxPolyDP(best, 0.0005 * cv.arcLength(best, True), True)
return best


def refine_instance(im, box, cnt, cls):
h, w = im.shape[:2]
x1, y1, x2, y2 = box

# --- 1️⃣ Expand box by 10% ---
box_w = x2 - x1
box_h = y2 - y1
expand_x = int(0.2 * box_w)
expand_y = int(0.2 * box_h)

x1_exp, y1_exp = x1 - expand_x, y1 - expand_y
x2_exp, y2_exp = x2 + expand_x, y2 + expand_y

# --- 2️⃣ Clip to image bounds ---
x1_clip, y1_clip = max(x1_exp, 0), max(y1_exp, 0)
x2_clip, y2_clip = min(x2_exp, w), min(y2_exp, h)

# --- 3️⃣ Extract valid region ---
roi = im[y1_clip:y2_clip, x1_clip:x2_clip]

# White background of expanded size
roi_h, roi_w = y2_exp - y1_exp, x2_exp - x1_exp
white_bg = np.ones((roi_h, roi_w, 3), dtype=np.uint8) * 255

y_offset = y1_clip - y1_exp
x_offset = x1_clip - x1_exp
white_bg[y_offset:y_offset + roi.shape[0], x_offset:x_offset + roi.shape[1]] = roi

# --- 4️⃣ Contour alignment (relative to expanded ROI) ---
xs, ys = cnt[0], cnt[1]
# assert xs.shape == ys.shape, "xs and ys must have same length"
cnt_np = np.stack([xs, ys], axis=1).astype(np.float32)

cnt_np[:, 0] -= x1_exp
cnt_np[:, 1] -= y1_exp

# --- 5️⃣ Scale so that longest side = 1024 ---
long_side = max(roi_w, roi_h)
scale = 1024 / long_side
new_w = int(roi_w * scale)
new_h = int(roi_h * scale)

roi_scaled = cv.resize(white_bg, (new_w, new_h), interpolation=cv.INTER_LINEAR)
cnt_scaled = cnt_np * scale

# --- 6️⃣ Pad to 1024×1024 with white margins ---
pad_x = (1024 - new_w) // 2
pad_y = (1024 - new_h) // 2

roi_padded = np.ones((1024, 1024, 3), dtype=np.uint8) * 255
roi_padded[pad_y:pad_y + new_h, pad_x:pad_x + new_w] = roi_scaled

# Adjust contour for padding (relative to final ROI)
cnt_final = cnt_scaled + np.array([pad_x, pad_y], dtype=np.float32)

# --- 7️⃣ Keep mapping info to reconstruct original coordinates ---
transform_info = {
"x1_exp": x1_exp,
"y1_exp": y1_exp,
"scale": scale,
"pad_x": pad_x,
"pad_y": pad_y
}


pts = np.array(cnt_final, dtype=np.int32).reshape(-1, 2)

refined_candidate = yolo_ensemble_contour( roi_padded, pts)

cv.polylines(roi_padded, [pts], isClosed=True, color=(0, 0, 255), thickness=2)
cv.polylines(roi_padded, [refined_candidate], isClosed=True, color=(255, 0, 0), thickness=2)
cv.imshow("test", roi_padded)
cv.waitKey(-1)



def refine_file(path):
with open(path, 'r') as f:
data = json.load(f)

dir_name = os.path.dirname(path)
parent_image_rel_path = data["image_path"]
# parent_image_abs_path = os.path.join(dir_name, parent_image_rel_path)
parent_image_abs_path = os.path.join( parent_image_rel_path)
assert os.path.isfile(parent_image_abs_path), parent_image_abs_path

im = cv.imread(parent_image_abs_path)


for box, cnt, cls in zip(data["boxes"], data["contours"], data["classes"]):
refine_instance(im, box, cnt, cls)


if __name__ == "__main__":
# main()
from ultralytics import YOLO
from ultralytics.engine.results import Results
model_file = "flat_bug_S.pt"
result_file = "data/metadata_mask-refiner-test3_UUID_ChangeThisTEMPORARY.json"
yolo = YOLO(model_file, "segment", verbose=True)
refine_file(result_file)
25 changes: 14 additions & 11 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,18 @@ classifiers = [
dependencies = [
"torch>=2.11",
"torchvision>=0.17.0",
"ultralytics>=8.2.16,<=8.3.124",
"ultralytics>=8.4.0,<=8.4.90",
"shapely>=2.0.2",
"scikit-optimize>=0.10.1",
"scipy>=1.14.1"
"scipy>=1.14.1",
"tqdm>=4.0.0",
]
keywords=['deep learning', 'object detection', 'instance segmentation', 'arthropods']

[project.optional-dependencies]
eval = [
"pandas",
]
erda = [
"pyremotedata>=0.0.16"
]
Expand All @@ -48,24 +52,23 @@ fb_prepare_data = "flat_bug.cli.fb_prepare_data:main"
fb_clone_data = "flat_bug.cli.fb_clone_data:main"

[dependency-groups]
docs = [
"furo>=2025.12.19",
"myst-parser>=5.0.0",
"sphinx>=9.0.4",
]
notebook = [
dev = [
"ipykernel>=7.2.0",
"ipywidgets>=8.1.8",
"jupyter>=1.0.0",
]
tests = [
"pytest>=9.0.3",
"ruff>=0.15.12",
]
docs = [
"furo>=2025.12.19",
"myst-parser>=5.0.0",
"sphinx>=9.0.4",
"sphinx_rtd_theme"
]

[tool.ruff]
target-version = "py311"
line-length = 140
line-length = 120
extend-exclude = [
"utils",
"scripts",
Expand Down
2 changes: 1 addition & 1 deletion scripts/training/fb_config_M40S.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
batch: 8
model: "./yolov8m-seg.pt"
model: "yolo26m-seg.pt"
epochs: 100
device: [0, 1]
patience: 9999
Expand Down
2 changes: 1 addition & 1 deletion scripts/training/fb_config_M40S_GHPC.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
batch: 8
model: "yolov8m-seg.pt"
model: "yolo26m-seg.pt"
epochs: 500
device: 0
patience: 9999
Expand Down
5 changes: 2 additions & 3 deletions scripts/training/fb_config_N40S.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
batch: 8
model: "./yolov8n-seg.pt"
# model: "yolov26n.pt"
epochs: 100
device: [0, 1]
device: [0]
patience: 9999
lr0: 0.01
lrf: 0.0001
Expand All @@ -11,6 +11,5 @@ fb_max_instances: 9999
fb_max_images: -1
fb_custom_eval: false
fb_custom_eval_num_images: 100
fb_exclude_datasets: ["00-prospective-ALUS-mixed", "00-prospective-crall2023", "00-prospective-chavez2024", "00-prospective-InsectCV"] # "01-partial-NHM-beetles-crops", "01-partial-Diopsis", "01-partial-abram2023", "01-partial-AMI-traps", "01-partial-gernat2018"
plots: true

11 changes: 11 additions & 0 deletions scripts/training/fb_config_yolo26n.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
model: "yolo26n-seg.pt"
batch: 8
epochs: 5000
device: "cuda"
patience: 9999
workers: 4
lr0: 0.001
lrf: 0.00001
optimizer: "SGD"
fb_max_instances: 150
name: "yolo26n"
19 changes: 14 additions & 5 deletions scripts/training/train.sh
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,18 @@
# # SBATCH -t 96:00:00

#CONFIG=fb_config_L40S_fine-tune.yaml
CONFIG=fb_config_S40S.yaml
ROOT=/home/altair/flat-bug
# source ${ROOT}/.venv/bin/activate
# fb_prepare_data.py -i ${ROOT}/flat-bug-data/pre-pro/ -o ${ROOT}/flat-bug-data/yolo/ -f
CONFIG=fb_config_N40S.yaml

# Derive model size letter (N/S/M/L/X) from config filename, e.g. fb_config_N40S.yaml -> N
SIZE="N"
NAME="fb_${SIZE}_$(date +%Y-%m-%d_%H-%M-%S)"

ROOT=~/Desktop/flatbug-dir/
#fb_clone_data -s ~/flat-bug/repos/scripts/training/.secrets.yaml -o ${ROOT}/flat-bug-data/pre-pro/
#fb_prepare_data -i ${ROOT}/flat-bug-data/pre-pro/ -o ${ROOT}/flat-bug-data/yolo/ -f
fb_train -c ${CONFIG} -d ${ROOT}/flat-bug-data/yolo/ --name ${NAME}


#fb_prepare_data.py -i ${ROOT}/flat-bug-data/pre-pro/ -o ${ROOT}/flat-bug-data/yolo/ -f
# fb_train.py -c ${ROOT}/scripts/training/${CONFIG} -d ${ROOT}/flat-bug-data/yolo/
fb_train -c ${ROOT}/scripts/training/${CONFIG} -d dev/fb_yolo
#fb_train -c ${ROOT}/scripts/training/${CONFIG} -d dev/fb_yolo
Loading