Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ Based on [ggml](https://github.com/ggml-org/ggml) similar to the [llama.cpp](htt
| [**MI-GAN**](#mi-gan) | Inpainting | CPU, Vulkan |
| [**ESRGAN**](#real-esrgan) | Super-resolution | CPU, Vulkan |
| [**YOLOv9t**](#yolov9t) | Object detection | CPU |
| [**MMDetection** models](docs/mmdet-detectors.md) | Object detection, segmentation, tracking | CPU |
| [_Implement a model [**Guide**]_](docs/model-implementation-guide.md) | | |

**Backbones:** SWIN (v1), DINO (v2), TinyViT
Expand Down
504 changes: 504 additions & 0 deletions docs/mmdet-detectors.md

Large diffs are not rendered by default.

288 changes: 151 additions & 137 deletions tools/README.md

Large diffs are not rendered by default.

24 changes: 21 additions & 3 deletions tools/build/build_mmdet_cpp.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@
# head.cpp 는 라이브러리가 아니라 여기서 러너와 함께 컴파일된다 → g2c output/.cpp 를 직접
# 컴파일(arch/ 복사·cli REG 없음).
#
# 사용: build_mmdet_cpp.sh <gen_dir> [arch_name]
# usage: build_mmdet_cpp.sh <gen_dir> <params.h> [arch_name]
# gen_dir = g2c --output 디렉토리 (예: output/MMDetBackbone) — <ARCH>.cpp/.h/.gguf 있음
# params.h = <name>.postproc.h written by mmdet_to_pt.py next to its --out. When omitted,
# a *.postproc.h inside gen_dir is used.
# arch_name= 클래스명(생략 시 gen_dir 의 *.cpp 에서 자동)
# env: VISP_BUILD = libvisioncpp 빌드 디렉토리 (기본: <vision.cpp>/build)
#
Expand All @@ -17,7 +19,22 @@ DETECT="$V/tools/detect" # 공용 head/decode 부품 (head.cpp/
RUN="$V/tools/verify" # E2E 검증 러너
GEN="${1:?usage: build_mmdet_cpp.sh <gen_dir> [arch_name]}"
GEN="$(cd "$GEN" && pwd)"
ARCH="${2:-}"
# The second argument is either the parameters header (.h) or an architecture name.
PARAMS=""
ARCH=""
case "${2:-}" in
*.h) PARAMS="$(cd "$(dirname "$2")" && pwd)/$(basename "$2")"; ARCH="${3:-}" ;;
"") ;;
*) ARCH="$2" ;;
esac
if [ -z "$PARAMS" ]; then
PARAMS="$(ls "$GEN"/*.postproc.h 2>/dev/null | head -1)"
fi
if [ ! -f "$PARAMS" ]; then
echo "no parameters header. mmdet_to_pt.py writes <name>.postproc.h next to its --out."
echo " usage: $(basename "$0") $GEN <name>.postproc.h"
exit 1
fi
if [ -z "$ARCH" ]; then
ARCH="$(basename "$(ls "$GEN"/*.cpp | grep -v run_ | head -1)" .cpp)"
fi
Expand All @@ -31,14 +48,15 @@ FMT_INC="$BUILD/_deps/fmt-src/include"
FMT_FLAGS=""
[ -f "$FMT_INC/fmt/format.h" ] && FMT_FLAGS="-DVISP_FMT_LIB -I$FMT_INC"

echo "arch=$ARCH gen=$GEN build=$BUILD fmt=${FMT_FLAGS:-fallback}"
echo "arch=$ARCH gen=$GEN params=$PARAMS build=$BUILD fmt=${FMT_FLAGS:-fallback}"
INC="$GEN/inc"
mkdir -p "$INC/visp/arch"
cp "$GEN/$ARCH.h" "$INC/visp/arch/$ARCH.h"

# run_mmdet.cpp + head.cpp(러너와 함께 컴파일, 라이브러리 아님) + g2c 백본 output/.cpp
g++ -std=c++20 -O2 $FMT_FLAGS \
-DARCH="$ARCH" -DVISP_ARCH_HEADER="\"visp/arch/$ARCH.h\"" \
-DMMDET_PARAMS_HEADER="\"$PARAMS\"" \
-I"$DETECT" -I"$INC" -I"$V/include" -I"$V/src" \
-I"$V/depend/llama/ggml/include" -I"$V/depend/llama/vendor" \
"$RUN/backbone/run_mmdet.cpp" "$DETECT/head.cpp" "$GEN/$ARCH.cpp" \
Expand Down
76 changes: 76 additions & 0 deletions tools/detect/draw.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// Draw detections onto an image -- the minimum needed for the runner's default output.
//
// No text is drawn. A font would grow the runner for nothing, because the runner also prints
// the detections as a table: the image carries where, the table carries what.
#pragma once

#include "visp/image.h"
#include "visp/postproc.h"

#include <algorithm>
#include <cstdint>
#include <vector>

namespace visp {

// Enough separation to tell classes apart. Same class, same colour, no legend needed.
inline std::array<uint8_t, 3> detection_colour(int label) {
static constexpr uint8_t table[][3] = {
{230, 60, 60}, {60, 160, 230}, {70, 190, 110}, {240, 160, 40}, {170, 100, 220},
{40, 200, 200}, {230, 110, 170}, {150, 160, 60}, {110, 130, 240}, {200, 90, 60},
};
int n = int(sizeof(table) / sizeof(table[0]));
int i = ((label % n) + n) % n;
return {table[i][0], table[i][1], table[i][2]};
}

namespace detail {

inline void put_pixel(image_span const& img, int x, int y, std::array<uint8_t, 3> c) {
if (x < 0 || y < 0 || x >= img.extent[0] || y >= img.extent[1]) {
return;
}
int nc = n_channels(img.format);
auto* p = static_cast<uint8_t*>(img.data) + size_t(y) * img.stride + size_t(x) * nc;
p[0] = c[0];
if (nc > 1) p[1] = c[1];
if (nc > 2) p[2] = c[2];
}

} // namespace detail

// One box outline, drawn inwards to the given thickness in pixels.
inline void draw_box(image_span const& img, float x1, float y1, float x2, float y2,
std::array<uint8_t, 3> colour, int thickness = 2) {
int ix1 = int(std::min(x1, x2)), ix2 = int(std::max(x1, x2));
int iy1 = int(std::min(y1, y2)), iy2 = int(std::max(y1, y2));
for (int t = 0; t < thickness; ++t) {
for (int x = ix1; x <= ix2; ++x) {
detail::put_pixel(img, x, iy1 + t, colour);
detail::put_pixel(img, x, iy2 - t, colour);
}
for (int y = iy1; y <= iy2; ++y) {
detail::put_pixel(img, ix1 + t, y, colour);
detail::put_pixel(img, ix2 - t, y, colour);
}
}
}

// Draw a list of detections. Coordinates are in the square input the detector ran on, so
// scale_x / scale_y put them back on the original image. Returns how many were drawn.
inline int draw_detections(image_span const& img, std::vector<detection> const& dets,
float scale_x, float scale_y, float threshold = 0.3f) {
int thickness = std::max(2, img.extent[1] / 300);
int drawn = 0;
for (detection const& d : dets) {
if (d.score < threshold) {
continue;
}
draw_box(img, d.x1 * scale_x, d.y1 * scale_y, d.x2 * scale_x, d.y2 * scale_y,
detection_colour(d.label), thickness);
++drawn;
}
return drawn;
}

} // namespace visp
13 changes: 13 additions & 0 deletions tools/detect/head.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#pragma once

#include "visp/ml.h"
#include "visp/postproc.h" // det_params

#include <string>
#include <vector>
Expand All @@ -24,6 +25,18 @@ struct anchor_head_cfg {
bool head_has_norm = false; // 타워에 norm(GN 등) — 이번 PoC(RetinaNet)=false
};

// Everything the runner needs to run one detector.
// Once an architecture is fixed these are constants, so mmdet_to_pt.py emits them as
// mmdet_params() in <name>.postproc.h and they are compiled into the runner. Nothing is read
// at run time.
struct mmdet_cfg {
anchor_head_cfg head;
det_params det;
float img_mean[3] = {0, 0, 0};
float img_std[3] = {1, 1, 1};
bool to_rgb = false;
};

// FPN features(레벨별, cwhn) → 레벨별 raw cls_score / bbox_pred(cwhn).
// · cls_out[l] : ne={num_base*num_classes, feat_w, feat_h, 1}
// · box_out[l] : ne={num_base*4, feat_w, feat_h, 1}
Expand Down
88 changes: 82 additions & 6 deletions tools/frontend/mmdet/mmdet_to_pt.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
PYTHONPATH=<GTX_Compiler>:<이 폴더> g2c --model retinanet_bb.pt --name Retina --output output/retina
"""
import argparse
import json
import math
import os
import sys
import torch
Expand All @@ -23,6 +23,81 @@
from mmdet_wrap import MMDetBackbone, build # noqa: E402,F401 (MMDetBackbone: 피클 등록)


def _f(v):
"""A C++ float literal. repr keeps the significant digits.

Infinities reach here from configs that bound a regression range with one -- FCOS writes
regress_ranges=((-1, 64), ..., (512, INF)) -- and `inff` is not something C++ accepts.
"""
v = float(v)
if math.isinf(v):
return "-INFINITY" if v < 0 else "INFINITY"
if math.isnan(v):
return "NAN"
return f"{v!r}f"


def _arr(name, values):
return f" c.det.{name} = {{{', '.join(_f(v) for v in values)}}};\n"


def emit_params(cfg, config_name):
"""Decoding and anchor configuration as a C++ header holding mmdet_params().

Replaces the JSON sidecar. The values end up inside the executable, which removes a
deployed file and makes it impossible to pair a .gguf with a configuration from a
different export.
"""
h = cfg.get("head_type", "raw")
out = [
"// Generated by mmdet_to_pt.py — do not edit.\n",
f"// source: {config_name}\n",
f"// head_type: {h}\n",
"#pragma once\n\n",
"#include <cmath>\n",
'#include "head.h"\n\n',
"namespace visp {\n\n",
"inline mmdet_cfg mmdet_params() {\n",
" mmdet_cfg c;\n",
]
if h != "anchor":
out += [
" // The head in this config was not recognised, so only the backbone is\n",
" // exported and decoding is left to the caller.\n",
" return c;\n}\n\n} // namespace visp\n",
]
return "".join(out)

for k in ("stacked_convs", "feat_channels", "num_base", "num_classes"):
if k in cfg:
out.append(f" c.head.{k} = {int(cfg[k])};\n")
for k in ("cls_convs_prefix", "reg_convs_prefix", "cls_head", "reg_head"):
if k in cfg:
out.append(f' c.head.{k} = "{cfg[k]}";\n')
out.append(f" c.head.head_has_norm = {str(bool(cfg.get('head_has_norm', False))).lower()};\n\n")

out.append(_arr("strides", cfg["strides"]))
out.append(_arr("octave_scales", cfg["octave_scales"]))
out.append(_arr("ratios", cfg["ratios"]))
out.append(f" c.det.octave_base_scale = {_f(cfg.get('octave_base_scale', 4.0))};\n")
out.append(f" c.det.center_offset = {_f(cfg.get('center_offset', 0.0))};\n")
for i, v in enumerate(cfg.get("means", [0.0] * 4)):
out.append(f" c.det.means[{i}] = {_f(v)};\n")
for i, v in enumerate(cfg.get("stds", [1.0] * 4)):
out.append(f" c.det.stds[{i}] = {_f(v)};\n")
out.append(f" c.det.num_classes = {int(cfg.get('num_classes', 80))};\n")
out.append(f" c.det.use_sigmoid = {str(bool(cfg.get('use_sigmoid', True))).lower()};\n\n")

for i, v in enumerate(cfg.get("img_mean", [0.0] * 3)):
out.append(f" c.img_mean[{i}] = {_f(v)};\n")
for i, v in enumerate(cfg.get("img_std", [1.0] * 3)):
out.append(f" c.img_std[{i}] = {_f(v)};\n")
out.append(f" c.to_rgb = {str(bool(cfg.get('to_rgb', False))).lower()};\n")

out.append(" return c;\n}\n\n} // namespace visp\n")
return "".join(out)


def main(argv=None):
ap = argparse.ArgumentParser(prog="mmdet_to_pt")
ap.add_argument("--config", required=True, help="mmdet config .py")
Expand All @@ -37,11 +112,12 @@ def main(argv=None):
n_head = sum(1 for k in m.state_dict() if k.startswith("bbox_head"))
print(f" → saved: {a.out} (state_dict {len(m.state_dict())} tensors, head {n_head} 포함)")

# decode/anchor + head-conv config 사이드카 (vision.cpp mmdet 부품이 읽음)
sidecar = os.path.splitext(a.out)[0] + ".postproc.json"
with open(sidecar, "w") as f:
json.dump(cfg, f, indent=2)
print(f" → sidecar: {sidecar} (head_type={cfg.get('head_type')})")
# Decoding, anchor and head-conv configuration, emitted as a C++ function. Once the
# architecture is fixed these are constants, so they are compiled into the runner.
header = os.path.splitext(a.out)[0] + ".postproc.h"
with open(header, "w") as f:
f.write(emit_params(cfg, os.path.basename(a.config)))
print(f" → params: {header} (head_type={cfg.get('head_type')})")


if __name__ == "__main__":
Expand Down
Loading
Loading