Skip to content
Merged
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
4 changes: 2 additions & 2 deletions docs/TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

| Level | Command / workflow | What it verifies | What it does not verify |
| --- | --- | --- | --- |
| Python unit tests | `cd inference && python -m unittest discover -s tests -v` | Confidence handling, model path and local URL restrictions using mock objects | Actual trained model or image decoding |
| Python unit tests | `cd inference && python -m unittest discover -s tests -v` | Confidence handling, model path and local URL restrictions, plus independent concurrent image request outputs using mock objects | Actual trained model or image decoding |
| Vue build | `cd frontend && npm ci && npm run build` | The frontend bundle compiles | Browser integration or API responses |
| Spring Boot checks | `cd backend && mvn -Dtest=DeepSeekControllerTest,FileControllerTest package` | Backend compilation, selected input/control tests | Database integration or paid AI connectivity |
| Documentation links | `python scripts/check_docs_links.py` | Relative Markdown links resolve | External link availability |
Expand All @@ -23,4 +23,4 @@ The optional test installs CPU PyTorch/Ultralytics packages and may download sub

## Before claiming production readiness

Run complete image/video/camera inference against the local Spring Boot upload/database flow, verify model-class mappings and nonempty/empty detections, profile CPU/GPU resource usage, test overlapping user sessions, audit upload authentication and content validation, and document the provenance/redistribution permissions of sample data and pretrained model assets.
The image route now uses per-request output paths and no longer mutates shared video state; unit tests simulate overlapping image calls. This does not prove end-to-end concurrency or cover the still-shared video/camera state. Run complete image/video/camera inference against the local Spring Boot upload/database flow, verify model-class mappings and nonempty/empty detections, profile CPU/GPU resource usage, test overlapping user sessions, audit upload authentication and content validation, and document the provenance/redistribution permissions of sample data and pretrained model assets.
58 changes: 58 additions & 0 deletions inference/image_requests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
"""Per-request image prediction orchestration for the local CropLens demo.

The video/camera routes still maintain their own shared mutable state and
need a separate concurrency review; this helper only isolates images.
"""
import json
import uuid
from pathlib import Path


def process_image(data, weight_path, threshold, predictor_class, upload, output_dir="./runs"):
"""Predict one image and return the existing JSON-compatible response shape.

All mutable state and the rendered output filename are local to a single
request; the temporary image is removed even if prediction/upload fails.
"""
response = {
"username": data.get("username"),
"weight": data["weight"],
"conf": data["conf"],
"startTime": data.get("startTime"),
"inputImg": data["inputImg"],
"kind": data["kind"],
}
folder = Path(output_dir)
folder.mkdir(parents=True, exist_ok=True)
output = folder / ("result_" + uuid.uuid4().hex + ".jpg")
try:
predictor = predictor_class(
weights_path=weight_path,
img_path=response["inputImg"],
save_path=str(output),
kind=response["kind"],
conf=threshold,
)
result = predictor.predict()
if result["labels"] == "预测失败":
response.update({"status": 400, "message": "该图片无法识别,请重新上传!"})
return response
if not output.is_file():
response.update({"status": 400, "message": "预测结果图片未生成"})
return response
url = upload(str(output))
if not url:
response.update({"status": 400, "message": "预测图片上传失败"})
return response
response.update({
"status": 200,
"message": "预测成功",
"outImg": url,
"allTime": result["allTime"],
"confidence": json.dumps(result["confidences"]),
"label": json.dumps(result["labels"], ensure_ascii=False),
})
return response
finally:
if output.is_file():
output.unlink()
37 changes: 9 additions & 28 deletions inference/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from ultralytics import YOLO
from predict import predictImg
from input_validation import resolve_weight_file, validate_local_media_url
from image_requests import process_image
from flask_socketio import SocketIO, emit


Expand Down Expand Up @@ -71,34 +72,14 @@ def predictImg(self):
raise ValueError("Invalid crop type")
except (ValueError, TypeError):
return json.dumps({"status": 400, "message": "无效的模型、图片地址或预测参数"}, ensure_ascii=False), 400
self.data.clear()
self.data.update({
"username": data['username'], "weight": data['weight'],
"conf": data['conf'], "startTime": data['startTime'],
"inputImg": data['inputImg'],
"kind": data['kind']
})
print(self.data)
predict = predictImg.ImagePredictor(weights_path=weight_path,
img_path=self.data["inputImg"], save_path='./runs/result.jpg', kind=self.data["kind"],
conf=threshold)
# 执行预测
results = predict.predict()
uploadedUrl = self.upload('./runs/result.jpg')
if results['labels'] != '预测失败':
self.data["status"] = 200
self.data["message"] = "预测成功"
self.data["outImg"] = uploadedUrl
self.data["allTime"] = results['allTime']
self.data["confidence"] = json.dumps(results['confidences'])
self.data["label"] = json.dumps(results['labels'])
else:
self.data["status"] = 400
self.data["message"] = "该图片无法识别,请重新上传!"
path = self.data["inputImg"].split('/')[-1]
if os.path.exists('./' + path):
os.remove('./' + path)
return json.dumps(self.data, ensure_ascii=False)
response = process_image(
data=data,
weight_path=weight_path,
threshold=threshold,
predictor_class=predictImg.ImagePredictor,
upload=self.upload,
)
return json.dumps(response, ensure_ascii=False)

def predictVideo(self):
"""视频流处理接口"""
Expand Down
107 changes: 107 additions & 0 deletions inference/tests/test_image_requests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
"""Regression tests for request-local image results, without torch or Flask."""
import tempfile
import unittest
from concurrent.futures import ThreadPoolExecutor
from pathlib import Path
from threading import Barrier

from image_requests import process_image


def payload(image):
return {
"username": image,
"weight": "wheat_best.pt",
"conf": "0.5",
"startTime": "2026-10-08",
"inputImg": image,
"kind": "wheat",
}


class ImageRequestTests(unittest.TestCase):
def setUp(self):
self.directory = tempfile.TemporaryDirectory()
self.addCleanup(self.directory.cleanup)
self.root = Path(self.directory.name)

def test_success_uses_unique_file_and_cleans_up(self):
class Predictor:
def __init__(self, **params):
self.params = params

def predict(self):
Path(self.params["save_path"]).write_text("rendered", encoding="utf-8")
return {"labels": ["Leaf_Rust"], "confidences": ["73.00%"], "allTime": "0.2秒"}

captured = []

def upload(filename):
captured.append(Path(filename).read_text(encoding="utf-8"))
return "http://localhost:9999/files/test.jpg"

result = process_image(payload("image-a"), "/weights/wheat_best.pt", 0.5, Predictor, upload, self.root)
self.assertEqual(result["status"], 200)
self.assertEqual(result["username"], "image-a")
self.assertEqual(result["outImg"], "http://localhost:9999/files/test.jpg")
self.assertEqual(result["confidence"], '["73.00%"]')
self.assertEqual(captured, ["rendered"])
self.assertEqual(list(self.root.iterdir()), [])

def test_no_detection_does_not_upload(self):
class Predictor:
def __init__(self, **kwargs):
pass

def predict(self):
return {"labels": "预测失败", "confidences": "0.00%", "allTime": "0.1秒"}

result = process_image(
payload("empty"), "/weights/wheat_best.pt", 0.5, Predictor,
lambda _: self.fail("should not upload failed predictions"), self.root
)
self.assertEqual(result["status"], 400)
self.assertEqual(list(self.root.iterdir()), [])

def test_failed_upload_removes_rendered_file(self):
class Predictor:
def __init__(self, **kwargs):
self.file = kwargs["save_path"]

def predict(self):
Path(self.file).write_bytes(b"image")
return {"labels": ["Leaf_Rust"], "confidences": ["70.00%"], "allTime": "0.1秒"}

result = process_image(payload("x"), "model.pt", 0.5, Predictor, lambda _: None, self.root)
self.assertEqual(result["status"], 400)
self.assertEqual(list(self.root.iterdir()), [])

def test_parallel_requests_do_not_overwrite_each_other(self):
barrier = Barrier(2)

class Predictor:
def __init__(self, **kwargs):
self.filename = kwargs["save_path"]
self.image = kwargs["img_path"]

def predict(self):
Path(self.filename).write_text(self.image, encoding="utf-8")
barrier.wait(timeout=10)
return {"labels": [self.image], "confidences": ["75.00%"], "allTime": "0.1秒"}

def upload(filename):
return Path(filename).read_text(encoding="utf-8")

def predict(label):
return process_image(payload(label), "model.pt", 0.5, Predictor, upload, self.root)

with ThreadPoolExecutor(max_workers=2) as pool:
results = list(pool.map(predict, ["first", "second"]))
self.assertEqual([r["outImg"] for r in results], ["first", "second"])
self.assertEqual([r["username"] for r in results], ["first", "second"])
self.assertEqual([r["status"] for r in results], [200, 200])
self.assertEqual(list(self.root.iterdir()), [])


if __name__ == "__main__":
unittest.main()
Loading