Skip to the projects
krasnoshtanov.com evidence regenerated 2026-09-15

Oleksii Krasnoshtanov

I build computer‑vision and NLP systems, then keep them running.

Third‑year Applied Data Science & AI at Breda University of Applied Sciences. Four of the projects below shipped to someone other than a grader.

Tilburg, Netherlands · 9.1/10 · BUas Scholarship · EN / UK / RU

01 — Detection by Shadow BrabantHack 2026 · DEMCON deep tech track · won · team of three

The person is outside the frame. Their shadow isn’t.

720×480 frame · the light and the silhouette are a diagram; the boxes are real output · results/submission_v5_ensemble.csv

Given a frame where the pedestrian is entirely out of shot, predict the box they occupy anyway. The first model regressed the four corners directly. Re-describing the same box against the frame edge it hides behind — which side, how far out, how wide, how tall — is what moved the number, because a distance and a width cannot describe an impossible box the way two loose corners can.

The second head, which way they are walking, never became learnable: 0.606 against a 48.3% base rate. It abstains on almost every frame rather than guessing, and that is the honest outcome rather than a missing feature.

One mean box for all 1,692 training frames scores 0.0000, because the boxes sit on both sides. Splitting by side is what creates a floor at all — the same idea the model itself needed.

Repository

predict-the-mean floor, per side
0.4295
direct regression
0.4675
decomposed targets + TTA
0.6096
organisers’ hidden test set
0.626
predicted left of frame / right of frame
229 / 185
direction: declines to answer
407 / 414
02 — Emotion Timeline Content Intelligence Agency · Sep–Nov 2025 · rebuilt solo

Swap the transcriber and 38% of the timeline moves.

0:005:0010:0015:0020:0025:0030:0035:0040:0045:0050:00
both engines agreethey lead to different answers
benchmarks/pipeline/timeline.json

Speech to text, then chunk, then split into scenes, then classify each one. The figure above is what happens when you change only the first of those: the same classifier over a transcript from a different engine returns a different emotion for 38% of the seconds both engines covered. A component nobody thinks of as part of the model moves more than a third of the output.

There is a second disagreement underneath it. The classifier also runs twice on the same transcript — once on the Russian directly, once on an English translation — and those two agree on only 17 of the 47 scenes. Where they do agree, held-out accuracy is higher, which makes disagreement a place to look rather than a number to average away.

359 of the 3,096 seconds are covered by one transcriber only and are excluded from the agreement figure. Of the 428,331 training rows, 419,180 rebuild from source today: 9,151 synthetic rows did not survive, and the class they belonged to is therefore not compared.

Repository

agreement over shared seconds
61.9% of 2,737
scenes where the two classifiers agree
17 / 47
primary model, held-out accuracy
0.4816
word error rate, chosen STT
0.81%
training rows, in → out
552,821 → 428,331
03 — CV Pipeline Deployment Platform BUas group project, five people · Apr–Jun 2026

One codebase, three deployment targets, and a curve that collapsed twice before it worked.

  1. one trunktests · coverage gate · image build
  2. Docker Composea laptop
  3. Portaineron-premise host
  4. Azure Container Appscloud, blue-green
docs/evidence/training-run/run_metrics.json

A plant-organ segmentation service with the machinery around it: FastAPI behind auth, a Next.js front end for the researchers, Postgres, Prometheus, and Airflow DAGs running the data lifecycle — versioned staging, training on Azure ML, drift detection, and retraining triggered by a researcher flagging a bad prediction.

The part worth showing is the deployment: the same trunk ships to Docker Compose, to an on-premise Portainer host and to Azure Container Apps, with blue-green cutover and a health-gated rollback. I owned that and the CI.

The run recorded here reached 0.6693 only after val F1 collapsed to zero twice. The evidence is a build from 2026-06-08; the cloud endpoints are not claimed to be up today.

Repository

deployment targets, one trunk
3
tests, line coverage
430 · 92.6%
CI coverage gate
85%
best val F1, epoch 39 of 50
0.6693
04 — Root-tip robotics Individual project · Nov 2025 – Jan 2026 · Kaggle #2 of 77

A swept PID controller beat the reinforcement-learning one.

kp 20, ki 0, kd 1 — 136 stepskp 15, ki 0, kd 3 — 137 stepskp 20, ki 0, kd 3 — 142 stepskp 10, ki 0, kd 3 — 143 stepskp 15, ki 0, kd 5 — 145 stepskp 15, ki 0, kd 1 — 148 stepskp 15, ki 0, kd 3 — 166 stepskp 20, ki 0, kd 5 — 173 stepskp 10, ki 0, kd 1 — 204 stepskp 10, ki 0, kd 5 — 222 stepskp 10, ki 0.5, kd 5 — 238 stepskp 15, ki 0.5, kd 1 — 276 steps, never settles reliablykp 20, ki 0.5, kd 1 — 308 stepskp 15, ki 0.5, kd 3 — 322 stepskp 20, ki 0.5, kd 3 — 432 stepskp 10, ki 0.1, kd 5 — 445 stepskp 20, ki 0.5, kd 5 — 526 stepskp 10, ki 0.5, kd 3 — 529 stepskp 15, ki 0.1, kd 1 — 564 stepskp 15, ki 0.1, kd 3 — 601 stepskp 10, ki 0.5, kd 1 — 623 stepskp 10, ki 0.1, kd 1 — 636 stepskp 20, ki 0.1, kd 1 — 693 stepskp 10, ki 0.1, kd 3 — 699 stepskp 15, ki 0.1, kd 5 — 720 steps, never settles reliablykp 15, ki 0.5, kd 5 — 738 stepskp 20, ki 0.1, kd 3 — 915 stepskp 20, ki 0.1, kd 5 — 1035 steps episode 1: 21.815 mm after 300 stepsepisode 2: 12.742 mm after 300 stepsepisode 3: 13.556 mm after 300 stepsepisode 4: 4.729 mm after 80 stepsepisode 5: 14.437 mm after 300 stepsepisode 6: 23.308 mm after 300 stepsepisode 7: 19.275 mm after 300 stepsepisode 8: 4.963 mm after 86 stepsepisode 9: 21.872 mm after 300 stepsepisode 10: 12.037 mm after 300 steps
robot_control/pid/tuning_results.json

Find the root tips in an image of a petri dish, then drive a simulated Opentrons OT-2 pipette to each one and dispense. The vision half and the control half were built and benchmarked separately, which is how the comparison below became possible.

28 gain settings, five trials each. The best combination settles in 136 steps; the ones with integral gain oscillate and never settle. A PPO policy trained on the same task reached the 5 mm threshold in two episodes out of ten. Sweeping three numbers beat learning the controller, and saying so is more useful than picking the answer that sounds more modern.

The 28 runs cover 27 distinct settings, because one was swept twice by accident — and the two runs disagree by 21%, settling in 165.8 and 137.0 steps on identical gains. That is the run-to-run variance, so both are drawn rather than averaged into one cell. No plate images appear here or anywhere in the repository: the source imagery is NPEC research data and is not mine to republish. Everything drawn is simulator output.

Repository

gain settings swept, 5 trials each
28
best settling time
136 steps
PPO episodes reaching 5 mm
2 / 10
PPO mean final distance
14.873 ± 6.351 mm
05—07 — Also shipped three shorter ones, side by side

Three more that reached someone.

05 — MARBET event assistant

Two-person sprint · Apr – May 2025 · in production for the client

Two people, two weeks, and a client who put it into production.

A self-hosted retrieval assistant answering questions about a corporate event from the client’s own documents and nothing else. There is no data in the repository, deliberately — the event material was theirs, not mine to publish.

client documentspdf · docx · textsemantic chunks800 / 200 overlapFAISS indexmxbai-embedfetch 20, keep 10two-stage filterLlama 3.3 70Blocal, via Ollama
Answers come from the client's own documents and nothing else. Fetching wider than needed and then filtering is what stopped it answering from the model's own memory.

Repository

06 — UK-Transcribe

Personal project · Jul 2026 · running on a GPU rack

One GPU, one lock — the second upload waits instead of crashing.

Ukrainian lecture transcription I host myself, so the audio never leaves the machine and there is no per-minute cloud bill. It picks whichever GPU has the most free memory at startup.

uploadany containerffmpegmono 16 kHzone model, one locksecond upload queuesfaster-whisperCUDA 12 · large-v3segments outreal decode position
The lock is the whole design. Two uploads hitting one GPU is not slower, it is a crash — so the second one waits instead, and the progress bar reports where the decoder actually is rather than counting seconds.

Repository

07 — Dutch Sign Language trainer

Solo rebuild of a group project · May – Sep 2026

Two letters in the alphabet are movements, not shapes.

A real-time NGT fingerspelling trainer, rebuilt alone from a three-person university project with no shared commit history. The accuracy figures I recorded at the time are not backed by anything committed, so they are not quoted here.

webcam frameover a WebSocketMediaPipe21 hand landmarksresidual MLP24 letters that hold stillBiLSTMJ and Z, 30-frame window
Two letters in the alphabet are movements rather than shapes, so they cannot be classified from one frame. They get a sequence model of their own, and the router in front decides which one a frame belongs to.

Repository