From 56dc9fa3d2bf85b04b6ce1a52cbd2fb5c9a6d816 Mon Sep 17 00:00:00 2001 From: Anton Tetov <anton@tetov.se> Date: Wed, 22 Jun 2022 16:08:49 +0200 Subject: [PATCH] wip --- .vscode/launch.json | 39 ++++ data/vattenhallen.data | 6 +- data/veges.data | 6 +- src/farmbot_yolo/darknet.py | 341 ---------------------------------- src/farmbot_yolo/detect.py | 241 ++++++++++++++++-------- src/farmbot_yolo/location.py | 344 +++++++++++++++++------------------ src/farmbot_yolo/main.py | 193 +++++++++++--------- src/farmbot_yolo/move.py | 106 ++++------- src/farmbot_yolo/try.py | 14 -- static/distance.txt | 6 +- 10 files changed, 526 insertions(+), 770 deletions(-) create mode 100644 .vscode/launch.json delete mode 100644 src/farmbot_yolo/darknet.py delete mode 100644 src/farmbot_yolo/try.py diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..e63ed1d --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,39 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "detect", + "type": "python", + "request": "launch", + "module": "farmbot_yolo.detect", + "justMyCode": false, + "args": ["--draw"] + }, + { + "name": "download", + "type": "python", + "request": "launch", + "module": "farmbot_yolo.download", + "justMyCode": false, + "args":["tmp/"] + }, + { + "name": "location", + "type": "python", + "request": "launch", + "module": "farmbot_yolo.location", + "justMyCode": false, + }, + { + "name": "scan", + "type": "python", + "request": "launch", + "module": "farmbot_yolo.move", + "justMyCode": false, + "args": ["-m", "2"] + } + ] +} diff --git a/data/vattenhallen.data b/data/vattenhallen.data index f6812d7..2f64fda 100644 --- a/data/vattenhallen.data +++ b/data/vattenhallen.data @@ -1,6 +1,6 @@ classes= 7 -train = ../dataset/train.list -valid = ../dataset/test.list -names = /home/xzleo/farmbot/dataset/classes.txt +train = /home/bot/farmbot/farmbot_yolo/dataset/train.list +valid = /home/bot/farmbot/farmbot_yolo/dataset/test.list +names = /home/bot/farmbot/farmbot_yolo/dataset/classes.txt backup = backup diff --git a/data/veges.data b/data/veges.data index 174c19a..4e25f34 100644 --- a/data/veges.data +++ b/data/veges.data @@ -1,6 +1,6 @@ classes= 10 -train = /home/xzleo/farmbot/dataset/train.txt -valid = /home/xzleo/farmbot/dataset/test.txt -names = /home/xzleo/farmbot/dataset/classes.txt +train = /home/bot/farmbot/farmbot_yolo/dataset/train.txt +valid = /home/bot/farmbot/farmbot_yolo/dataset/test.txt +names = /home/bot/farmbot/farmbot_yolo/dataset/classes.txt backup = backup diff --git a/src/farmbot_yolo/darknet.py b/src/farmbot_yolo/darknet.py deleted file mode 100644 index c9c3116..0000000 --- a/src/farmbot_yolo/darknet.py +++ /dev/null @@ -1,341 +0,0 @@ -#!/usr/bin/env python3 - -""" -Don't touch the code! -Python 3 wrapper for identifying objects in images - -Running the script requires opencv-python to be installed (`pip install opencv-python`) -Directly viewing or returning bounding-boxed images requires scikit-image to be installed (`pip install scikit-image`) -Use pip3 instead of pip on some systems to be sure to install modules for python3 -""" - -from ctypes import * -import math -import random -import os - - -class BOX(Structure): - _fields_ = [("x", c_float), - ("y", c_float), - ("w", c_float), - ("h", c_float)] - - -class DETECTION(Structure): - _fields_ = [("bbox", BOX), - ("classes", c_int), - ("best_class_idx", c_int), - ("prob", POINTER(c_float)), - ("mask", POINTER(c_float)), - ("objectness", c_float), - ("sort_class", c_int), - ("uc", POINTER(c_float)), - ("points", c_int), - ("embeddings", POINTER(c_float)), - ("embedding_size", c_int), - ("sim", c_float), - ("track_id", c_int)] - -class DETNUMPAIR(Structure): - _fields_ = [("num", c_int), - ("dets", POINTER(DETECTION))] - - -class IMAGE(Structure): - _fields_ = [("w", c_int), - ("h", c_int), - ("c", c_int), - ("data", POINTER(c_float))] - - -class METADATA(Structure): - _fields_ = [("classes", c_int), - ("names", POINTER(c_char_p))] - - -def network_width(net): - return lib.network_width(net) - - -def network_height(net): - return lib.network_height(net) - - -def bbox2points(bbox): - """ - From bounding box yolo format - to corner points cv2 rectangle - """ - x, y, w, h = bbox - xmin = int(round(x - (w / 2))) - xmax = int(round(x + (w / 2))) - ymin = int(round(y - (h / 2))) - ymax = int(round(y + (h / 2))) - return xmin, ymin, xmax, ymax - - -def class_colors(names): - """ - Create a dict with one random BGR color for each - class name - """ - return {name: ( - random.randint(0, 255), - random.randint(0, 255), - random.randint(0, 255)) for name in names} - - -def load_network(config_file, data_file, weights, batch_size=1): - """ - load model description and weights from config files - args: - config_file (str): path to .cfg model file - data_file (str): path to .data model file - weights (str): path to weights - returns: - network: trained model - class_names - class_colors - """ - network = load_net_custom( - config_file.encode("ascii"), - weights.encode("ascii"), 0, batch_size) - metadata = load_meta(data_file.encode("ascii")) - class_names = [metadata.names[i].decode("ascii") for i in range(metadata.classes)] - colors = class_colors(class_names) - return network, class_names, colors - - -def print_detections(detections, coordinates=False): - print("\nObjects:") - for label, confidence, bbox in detections: - x, y, w, h = bbox - if coordinates: - print("{}: {}% (left_x: {:.0f} top_y: {:.0f} width: {:.0f} height: {:.0f})".format(label, confidence, x, y, w, h)) - else: - print("{}: {}%".format(label, confidence)) - - -def draw_boxes(detections, image, colors): - import cv2 - for label, confidence, bbox in detections: - left, top, right, bottom = bbox2points(bbox) - cv2.rectangle(image, (left, top), (right, bottom), colors[label], 1) - cv2.putText(image, "{} [{:.2f}]".format(label, float(confidence)), - (left, top - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.5, - colors[label], 2) - return image - - -def decode_detection(detections): - decoded = [] - for label, confidence, bbox in detections: - confidence = str(round(confidence * 100, 2)) - decoded.append((str(label), confidence, bbox)) - return decoded - -# https://www.pyimagesearch.com/2015/02/16/faster-non-maximum-suppression-python/ -# Malisiewicz et al. -def non_max_suppression_fast(detections, overlap_thresh): - boxes = [] - for detection in detections: - _, _, _, (x, y, w, h) = detection - x1 = x - w / 2 - y1 = y - h / 2 - x2 = x + w / 2 - y2 = y + h / 2 - boxes.append(np.array([x1, y1, x2, y2])) - boxes_array = np.array(boxes) - - # initialize the list of picked indexes - pick = [] - # grab the coordinates of the bounding boxes - x1 = boxes_array[:, 0] - y1 = boxes_array[:, 1] - x2 = boxes_array[:, 2] - y2 = boxes_array[:, 3] - # compute the area of the bounding boxes and sort the bounding - # boxes by the bottom-right y-coordinate of the bounding box - area = (x2 - x1 + 1) * (y2 - y1 + 1) - idxs = np.argsort(y2) - # keep looping while some indexes still remain in the indexes - # list - while len(idxs) > 0: - # grab the last index in the indexes list and add the - # index value to the list of picked indexes - last = len(idxs) - 1 - i = idxs[last] - pick.append(i) - # find the largest (x, y) coordinates for the start of - # the bounding box and the smallest (x, y) coordinates - # for the end of the bounding box - xx1 = np.maximum(x1[i], x1[idxs[:last]]) - yy1 = np.maximum(y1[i], y1[idxs[:last]]) - xx2 = np.minimum(x2[i], x2[idxs[:last]]) - yy2 = np.minimum(y2[i], y2[idxs[:last]]) - # compute the width and height of the bounding box - w = np.maximum(0, xx2 - xx1 + 1) - h = np.maximum(0, yy2 - yy1 + 1) - # compute the ratio of overlap - overlap = (w * h) / area[idxs[:last]] - # delete all indexes from the index list that have - idxs = np.delete(idxs, np.concatenate(([last], - np.where(overlap > overlap_thresh)[0]))) - # return only the bounding boxes that were picked using the - # integer data type - return [detections[i] for i in pick] - -def remove_negatives(detections, class_names, num): - """ - Remove all classes with 0% confidence within the detection - """ - predictions = [] - for j in range(num): - for idx, name in enumerate(class_names): - if detections[j].prob[idx] > 0: - bbox = detections[j].bbox - bbox = (bbox.x, bbox.y, bbox.w, bbox.h) - predictions.append((name, detections[j].prob[idx], (bbox))) - return predictions - - -def remove_negatives_faster(detections, class_names, num): - """ - Faster version of remove_negatives (very useful when using yolo9000) - """ - predictions = [] - for j in range(num): - if detections[j].best_class_idx == -1: - continue - name = class_names[detections[j].best_class_idx] - bbox = detections[j].bbox - bbox = (bbox.x, bbox.y, bbox.w, bbox.h) - predictions.append((name, detections[j].prob[detections[j].best_class_idx], bbox)) - return predictions - - -def detect_image(network, class_names, image, thresh=.5, hier_thresh=.5, nms=.45): # - """ - Returns a list with highest confidence class and their bbox - """ - pnum = pointer(c_int(0)) - predict_image(network, image) # image 需要什么类型 - detections = get_network_boxes(network, image.w, image.h, - thresh, hier_thresh, None, 0, pnum, 0) - #print_detections(detections, coordinates=True) - num = pnum[0] - if nms: - do_nms_sort(detections, num, len(class_names), nms) - predictions = remove_negatives(detections, class_names, num) - predictions = decode_detection(predictions) - free_detections(detections, num) - return sorted(predictions, key=lambda x: x[1]) - - -if os.name == "posix": - cwd = os.path.abspath(os.path.join(os.getcwd(), "..")) # the one in Alexy's repo use current path, changing by our project structure, - lib = CDLL(cwd + "/darknet/libdarknet.so", RTLD_GLOBAL) -elif os.name == "nt": - cwd = os.path.dirname(__file__) - os.environ['PATH'] = cwd + ';' + os.environ['PATH'] - lib = CDLL("darknet.dll", RTLD_GLOBAL) -else: - print("Unsupported OS") - exit - -lib.network_width.argtypes = [c_void_p] -lib.network_width.restype = c_int -lib.network_height.argtypes = [c_void_p] -lib.network_height.restype = c_int - -copy_image_from_bytes = lib.copy_image_from_bytes -copy_image_from_bytes.argtypes = [IMAGE,c_char_p] - -predict = lib.network_predict_ptr -predict.argtypes = [c_void_p, POINTER(c_float)] -predict.restype = POINTER(c_float) - -set_gpu = lib.cuda_set_device -init_cpu = lib.init_cpu - -make_image = lib.make_image -make_image.argtypes = [c_int, c_int, c_int] -make_image.restype = IMAGE - -get_network_boxes = lib.get_network_boxes -get_network_boxes.argtypes = [c_void_p, c_int, c_int, c_float, c_float, POINTER(c_int), c_int, POINTER(c_int), c_int] -get_network_boxes.restype = POINTER(DETECTION) - -make_network_boxes = lib.make_network_boxes -make_network_boxes.argtypes = [c_void_p] -make_network_boxes.restype = POINTER(DETECTION) - -free_detections = lib.free_detections -free_detections.argtypes = [POINTER(DETECTION), c_int] - -free_batch_detections = lib.free_batch_detections -free_batch_detections.argtypes = [POINTER(DETNUMPAIR), c_int] - -free_ptrs = lib.free_ptrs -free_ptrs.argtypes = [POINTER(c_void_p), c_int] - -network_predict = lib.network_predict_ptr -network_predict.argtypes = [c_void_p, POINTER(c_float)] - -reset_rnn = lib.reset_rnn -reset_rnn.argtypes = [c_void_p] - -load_net = lib.load_network -load_net.argtypes = [c_char_p, c_char_p, c_int] -load_net.restype = c_void_p - -load_net_custom = lib.load_network_custom -load_net_custom.argtypes = [c_char_p, c_char_p, c_int, c_int] -load_net_custom.restype = c_void_p - -free_network_ptr = lib.free_network_ptr -free_network_ptr.argtypes = [c_void_p] -free_network_ptr.restype = c_void_p - -do_nms_obj = lib.do_nms_obj -do_nms_obj.argtypes = [POINTER(DETECTION), c_int, c_int, c_float] - -do_nms_sort = lib.do_nms_sort -do_nms_sort.argtypes = [POINTER(DETECTION), c_int, c_int, c_float] - -free_image = lib.free_image -free_image.argtypes = [IMAGE] - -letterbox_image = lib.letterbox_image -letterbox_image.argtypes = [IMAGE, c_int, c_int] -letterbox_image.restype = IMAGE - -load_meta = lib.get_metadata -lib.get_metadata.argtypes = [c_char_p] -lib.get_metadata.restype = METADATA - -load_image = lib.load_image_color -load_image.argtypes = [c_char_p, c_int, c_int] -load_image.restype = IMAGE - -rgbgr_image = lib.rgbgr_image -rgbgr_image.argtypes = [IMAGE] - -predict_image = lib.network_predict_image -predict_image.argtypes = [c_void_p, IMAGE] -predict_image.restype = POINTER(c_float) - -predict_image_letterbox = lib.network_predict_image_letterbox -predict_image_letterbox.argtypes = [c_void_p, IMAGE] -predict_image_letterbox.restype = POINTER(c_float) - -network_predict_batch = lib.network_predict_batch -network_predict_batch.argtypes = [c_void_p, IMAGE, c_int, c_int, c_int, - c_float, c_float, POINTER(c_int), c_int, c_int] -network_predict_batch.restype = POINTER(DETNUMPAIR) - -if __name__ == "__main__": - net = load_network("/home/xzleo/farmbot/darknet/cfg/yolov3-veges-test.cfg", "/home/xzleo/farmbot/darknet/data/veges.data", "/home/xzleo/farmbot/darknet/backup/yolov3-veges_best.weights") - img = load_image(b"/home/xzleo/farmbot/dataset/2_a.png", 0, 0) - predictions = detect_image(net, img, thresh=.5, hier_thresh=.5, nms=.45) \ No newline at end of file diff --git a/src/farmbot_yolo/detect.py b/src/farmbot_yolo/detect.py index 5f32963..252b251 100644 --- a/src/farmbot_yolo/detect.py +++ b/src/farmbot_yolo/detect.py @@ -1,72 +1,98 @@ -''' +""" load images taken by the camera, return bounding boxes customized based on Alexy/darknet_images.py -''' -from argparse import ArgumentParser, Namespace -import os -import glob +""" +import json import random -# from typing_extensions import final -import darknet # darknet.py import time +from argparse import ArgumentParser +from argparse import Namespace +from pathlib import Path + import cv2 -import numpy as np +from darknet import darknet + +from farmbot_yolo import REPO +from farmbot_yolo import TMPDIR def check_arguments_errors(args): - assert 0 < args.thresh < 1, "Threshold should be a float between zero and one (non-inclusive)" - if not os.path.exists(args.config_file): - raise(ValueError("Invalid config path {}".format(os.path.abspath(args.config_file)))) - if not os.path.exists(args.weights): - raise(ValueError("Invalid weight path {}".format(os.path.abspath(args.weights)))) - if not os.path.exists(args.data_file): - raise(ValueError("Invalid data file path {}".format(os.path.abspath(args.data_file)))) - if args.input and not os.path.exists(args.input): - raise(ValueError("Invalid image path {}".format(os.path.abspath(args.input)))) - - -def load_images(images_path): + assert ( + 0 < args.thresh < 1 + ), "Threshold should be a float between zero and one (non-inclusive)" + if not args.config_file.exists(): + raise (ValueError(f"Invalid config path {args.config_file}")) + if not args.weights.exists(): + raise (ValueError(f"Invalid weight path {args.weight}")) + if not args.data_file.exists(): + raise (ValueError(f"Invalid data file path {args.data_file}")) + if args.input and not args.input.exists(): + raise (ValueError(f"Invalid image path {args.input}")) + + +def load_images(images_path: Path): """ If image path is given, return it directly For txt file, read it and return each line as image path In other case, it's a folder, return a list with names of each jpg, jpeg and png file """ - input_path_extension = images_path.split('.')[-1] - if input_path_extension in ['jpg', 'jpeg', 'png']: + VALID_IMAGE_EXTS = ("jpg", "jpeg", "png") + input_path_extension = images_path.suffix + if input_path_extension in VALID_IMAGE_EXTS: # single image return [images_path] - elif input_path_extension == "txt": + + if input_path_extension == "txt": with open(images_path, "r") as f: return f.read().splitlines() - else: + + if images_path.is_dir(): # folders - return glob.glob( - os.path.join(images_path, "*.jpg")) + \ - glob.glob(os.path.join(images_path, "*.png")) + \ - glob.glob(os.path.join(images_path, "*.jpeg")) + jpgs = list(images_path.glob("*.jpg")) + jpegs = list(images_path.glob("*.jpeg")) + pngs = list(images_path.glob("*.png")) + imgs = jpgs + jpegs + pngs + return [img for img in imgs if ".bbox" not in img.name] + + raise ValueError(f"Path not recognized as valid input. {images_path}") -def image_detection(image_path, network, class_names, class_colors, thresh): +def image_detection( + image_path, network, class_names, class_colors, thresh, draw_bbox=False +): # Darknet doesn't accept numpy images. # Create one with image we reuse for each detect - # add image.shape as the output + # add image.shape as the output width = darknet.network_width(network) height = darknet.network_height(network) darknet_image = darknet.make_image(width, height, 3) - image = cv2.imread(image_path) + image_path_str = str(image_path) + image = cv2.imread(image_path_str) image_rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) - image_resized = cv2.resize(image_rgb, (width, height), - interpolation=cv2.INTER_LINEAR) + image_resized = cv2.resize( + image_rgb, (width, height), interpolation=cv2.INTER_LINEAR + ) darknet.copy_image_from_bytes(darknet_image, image_resized.tobytes()) - detections = darknet.detect_image(network, class_names, darknet_image, thresh=thresh) + detections = darknet.detect_image( + network, class_names, darknet_image, thresh=thresh + ) darknet.free_image(darknet_image) - resized_image = darknet.draw_boxes(detections, image_resized, class_colors) - return image.shape, cv2.cvtColor(resized_image, cv2.COLOR_BGR2RGB), detections + image_w_bboxes = darknet.draw_boxes(detections, image_resized, class_colors) + + original_height = image.shape[1] + original_width = image.shape[0] + # fix aspect ratio + image_w_bboxes = cv2.resize(image_w_bboxes, (original_height, original_width)) + + # BGR -> RGB + image_w_bboxes = cv2.cvtColor(image_w_bboxes, cv2.COLOR_BGR2RGB) + + return image.shape, image_w_bboxes, detections def convert2relative(image, bbox): @@ -75,7 +101,7 @@ def convert2relative(image, bbox): """ x, y, w, h = bbox height, width, _ = image.shape - return x/width, y/height, w/width, h/height + return x / width, y / height, w / width, h / height def save_annotations(original_size, name, image, detections, class_names): @@ -84,25 +110,49 @@ def save_annotations(original_size, name, image, detections, class_names): oringinal_size is Ziliang's improvement """ height, width, _ = original_size - img_name = os.path.basename(name) - file_name = os.path.splitext(img_name)[0] + ".txt" - final_file_name = os.path.dirname(name) + '/annotations/' + file_name - with open(final_file_name, "w") as f: - for label, confidence, bbox in detections: - x, y, w, h = convert2relative(image, bbox) - label = class_names.index(label) - f.write("{} {:.4f} {:.4f} {:.4f} {:.4f} {:.4f}\n".format(label, x*width, y*height, w*width, h*height, float(confidence))) + annotations = [] + + for label, confidence, bbox in detections: + x, y, w, h = convert2relative(image, bbox) + + annotations.append( + { + "label": class_names.index(label), + "x": x * width, + "y": y * height, + "w": w * width, + "h": h * height, + "confidence": float(confidence), + } + ) + + metadata_file = name.with_suffix(".json") + + if metadata_file.exists() and metadata_file.stat().st_size > 0: + with metadata_file.open(mode="r") as fp: + img_dict = json.load(fp) + else: + img_dict = {} + + img_dict["detection_annotations"] = annotations + + with metadata_file.open(mode="w") as fp: + + json.dump(img_dict, fp) + + return metadata_file -def detect(args: Namespace)-> None: + +def detect(args: Namespace) -> None: check_arguments_errors(args) random.seed(3) # deterministic bbox colors network, class_names, class_colors = darknet.load_network( - args.config_file, - args.data_file, - args.weights, - batch_size=args.batch_size + str(args.config_file), + str(args.data_file), + str(args.weights), + batch_size=args.batch_size, ) images = load_images(args.input) @@ -113,43 +163,82 @@ def detect(args: Namespace)-> None: if args.input: if index >= len(images): break - image_name = images[index] + img_path = Path(images[index]) else: - image_name = input("Enter Image Path: ") + img_path = Path(input("Enter Image Path: ")) prev_time = time.time() original_size, resized_image, detections = image_detection( - image_name, network, class_names, class_colors, args.thresh - ) + img_path, network, class_names, class_colors, args.thresh + ) if args.save_labels: - save_annotations(original_size, image_name, resized_image, detections, class_names) + save_annotations( + original_size, img_path, resized_image, detections, class_names + ) + if args.draw: + cv2.imwrite(str(img_path.with_suffix(".bbox.jpg")), resized_image) darknet.print_detections(detections, args.ext_output) - fps = int(1/(time.time() - prev_time)) + fps = int(1 / (time.time() - prev_time)) print("FPS: {}".format(fps)) index += 1 if __name__ == "__main__": parser = ArgumentParser(description="YOLO Object Detection") - parser.add_argument("--input", type=str, default="../img", - help="image source. It can be a single image, a" - "txt with paths to them, or a folder. Image valid" - " formats are jpg, jpeg or png." - "If no input is given, ") - parser.add_argument("--batch_size", default=1, type=int, - help="number of images to be processed at the same time") - parser.add_argument("--weights", default="../weights/yolov3-vattenhallen_best.weights", - help="yolo weights path") - parser.add_argument("--ext_output", action='store_true', default=True, - help="display bbox coordinates of detected objects") - parser.add_argument("--save_labels", action='store_true', default=True, - help="save detections bbox for each image in yolo format") - parser.add_argument("--config_file", default="../cfg/yolov3-vattenhallen-test.cfg", - help="path to config file") - parser.add_argument("--data_file", default="../data/vattenhallen.data", - help="path to data file") - parser.add_argument("--thresh", type=float, default=.25, - help="remove detections with lower confidence") - parser.add_argument('-v', '--verbose', action='store_true', help='Verbose mode') + parser.add_argument( + "--input", + default=TMPDIR, + type=Path, + help="image source. It can be a single image, a" + "txt with paths to them, or a folder. Image valid" + " formats are jpg, jpeg or png." + "If no input is given, ", + ) + parser.add_argument( + "--batch_size", + default=1, + type=int, + help="number of images to be processed at the same time", + ) + parser.add_argument( + "--weights", + default=REPO / "weights/yolov3-vattenhallen_best.weights", + type=Path, + help="yolo weights path", + ) + parser.add_argument( + "--ext_output", + action="store_true", + default=True, + help="display bbox coordinates of detected objects", + ) + parser.add_argument( + "--save_labels", + action="store_true", + default=True, + help="save detections bbox for each image in yolo format", + ) + parser.add_argument( + "--config_file", + default=REPO / "cfg/yolov3-vattenhallen-test.cfg", + type=Path, + help="path to config file", + ) + parser.add_argument( + "--data_file", + type=Path, + default=REPO / "data/vattenhallen.data", + help="path to data file", + ) + parser.add_argument( + "--thresh", + type=float, + default=0.25, + help="remove detections with lower confidence", + ) + parser.add_argument( + "-d", "--draw", action="store_true", help="Save annotated images." + ) + parser.add_argument("-v", "--verbose", action="store_true", help="Verbose output") arguments = parser.parse_args() detect(arguments) diff --git a/src/farmbot_yolo/location.py b/src/farmbot_yolo/location.py index a9375b8..2f9e1ef 100644 --- a/src/farmbot_yolo/location.py +++ b/src/farmbot_yolo/location.py @@ -1,20 +1,20 @@ -''' -Author: Ziliang Xiong -This script loads intrinsci camera matrix, which has been determined by calibration with MATLAB. -It reads YOLO's bounding boxes and calculate their global 2D coordinate on the planting bed accodring to -coordinate transform. See details in README.md -''' +"""Convert coordinates local to photos to global coordinates. +This script loads intrinsci camera matrix, which has been determined by +calibration using MATLAB. It reads YOLO's bounding boxes and calculate their +global 2D coordinate on the planting bed accodring to coordinate transform. +""" +import json from argparse import ArgumentParser, Namespace -from logging import basicConfig, DEBUG, INFO, getLogger +from logging import DEBUG, INFO, basicConfig, getLogger from pathlib import Path -from numpy import array, ndarray, dot, squeeze +from typing import Optional, Tuple + +from numpy import array, dot, ndarray, squeeze from numpy.linalg import inv -from os import listdir -from os.path import join, isfile from scipy.io import loadmat -from typing import Tuple, Optional +from farmbot_yolo import LOGDIR, REPO, TMPDIR """Logger for log file""" _LOG = getLogger(__name__) @@ -25,234 +25,234 @@ BoundingBox = Tuple[float, float, float, float] KMatrix = ndarray(shape=(3, 3)) """Allowed input and output file formats.""" -_CAM_EXTENSIONS = 'mat' -_ANNOTATION_EXTENSIONS = 'txt' -_LOCATIONS_EXTENSIONS = 'txt' -_OFFSET_EXTENSIONS = 'txt' +_CAM_EXTENSIONS = "mat" +_ANNOTATION_EXTENSIONS = "txt" +_LOCATIONS_EXTENSIONS = "txt" +_OFFSET_EXTENSIONS = "txt" """Constant sweeping height""" -SWEEP_Z = 575 # change according to the setting, z=-100, height is 57.5cm +SWEEP_Z = 575 # change according to the setting, z=-100, height is 57.5cm + +def read_offsets(offset_path: Path) -> Tuple[Tuple[float, float]]: + """Load the file containing gripper and camera offset in relation to farmbot + encoder. -def read_offsets(offset_path: Path) -> Optional[Tuple[int, int]]: - ''' - Load the offsets file, for coordinate transformation + Parameters + ---------- + offset_path + file path - :param offset_path: file path - :return cam_offset: distance of the camera centroid to z axis of Farmbot (dx, dy) - gripper_offset: distance of the gripper centroid to z axis of Farmbot (dx, dy) - ''' + Returns + ------- + tuple of tuple of floats + distance of the camera centroid to z axis of Farmbot (dx, dy) & + distance of the gripper centroid to z axis of Farmbot (dx, dy) + """ if not offset_path.is_file(): - _LOG.error('{} is not a file or does not exist'.format(offset_path)) + _LOG.error("{} is not a file or does not exist".format(offset_path)) return None - - if not offset_path.suffix.lstrip('.') in _OFFSET_EXTENSIONS: - _LOG.error('{} must have an legal\ - extension: {}'.format(offset_path, _OFFSET_EXTENSIONS)) + + if not offset_path.suffix.lstrip(".") in _OFFSET_EXTENSIONS: + _LOG.error( + "{} must have an legal\ + extension: {}".format( + offset_path, _OFFSET_EXTENSIONS + ) + ) return None try: - with open(offset_path, 'r') as f: + with open(offset_path, "r") as f: offsets = f.readlines() except IOError: - _LOG.error('Unable to open input file {}.'.format(offset_path)) + _LOG.error("Unable to open input file {}.".format(offset_path)) return None cam_offset = (int(offsets[1]), int(offsets[2])) gripper_offset = (int(offsets[4]), int(offsets[5])) - _LOG.info('Load the gripper offset\n{}\n and the camera offset \n{}'.format(gripper_offset, cam_offset)) + _LOG.info( + "Load the gripper offset\n{}\n and the camera offset \n{}".format( + gripper_offset, cam_offset + ) + ) return cam_offset, gripper_offset -def load_cam_matrix(cam_path: Path) -> Optional[ndarray]: - ''' - load the mat file that contains camera calibration result, read the intrinsic matrix of the camera - :param cam_path: path of the mat file - :return intrinsic_matrix: K matrix of the camera - ''' - if not cam_path.suffix.lstrip('.') == _CAM_EXTENSIONS: - _LOG.error('{} has an illegal extension'.format(cam_path)) - return None - - try: - data = loadmat(cam_path) - except FileNotFoundError: - _LOG.error(' No such file') - return None - - intrinsic_matrix = data['camera_no_distortion'][0, 0][11] - _LOG.info('Load intrinsic_matrix of the camera \n{}'.format(intrinsic_matrix)) - return intrinsic_matrix +def load_cam_matrix(cam_path: Path) -> Optional[ndarray]: + """Load the mat file that contains camera calibration result. + Parameters + ---------- + cam_path + path of the mat file -def read_locations(locations_path: Path) -> Optional[ndarray]: - ''' - read the locations of farmbot that corresponds to each photo - - param: locations_path: the path of folder locations - return: list that contains locations - ''' - if not locations_path.is_dir(): - _LOG.error('{} is not a directory or does not exist'.format(locations_path)) - return None - - number_files = len(listdir(locations_path)) - if number_files != 1: - _LOG.error('More than one file of locations found the {}'.format(locations_path)) - return None - - locations_file = Path(locations_path, [file for file in listdir(locations_path)][0]) - if not locations_file.suffix.lstrip('.') in _LOCATIONS_EXTENSIONS: - _LOG.error('{} must have an legal\ - extension: {}'.format(locations_path, _LOCATIONS_EXTENSIONS)) + Returns + ------- + intrinsic_matrix + K matrix of the camera + """ + if not cam_path.suffix.lstrip(".") == _CAM_EXTENSIONS: + _LOG.error("{} has an illegal extension".format(cam_path)) return None try: - with open(locations_file, 'r') as f: - locations = f.readlines() - except IOError: - _LOG.error('Unable to open input file {}.'.format(locations_path)) + data = loadmat(cam_path) + except FileNotFoundError: + _LOG.error(" No such file") return None - list_location = [] - for location in locations: - X, Y, Z = location.split() - list_location.append([int(X), int(Y), int(Z)]) # integer??? - - _LOG.info('Load all the locations \n {}'.format(list_location)) - return array(list_location) + intrinsic_matrix = data["camera_no_distortion"][0, 0][11] + _LOG.info("Load intrinsic_matrix of the camera \n{}".format(intrinsic_matrix)) + return intrinsic_matrix def cam_coordinate(pixel_x: int, pixel_y: int, cam_matrix) -> Tuple[float, float]: - ''' + """ Project one object's pixel coordinate into the camera coordinate system Input: detection: a bounding box <x, y, w, h> inner_matrix: matrix K that contains focal length and other inner parameters Output: object's centroid location in camera coordinate - ''' - normalized_coordinate = dot(inv(cam_matrix.transpose()), array([pixel_x, pixel_y, 1], dtype=float).reshape((3, 1))) + """ + normalized_coordinate = dot( + inv(cam_matrix.transpose()), + array([pixel_x, pixel_y, 1], dtype=float).reshape((3, 1)), + ) camera_coordinate = squeeze(normalized_coordinate) ratio = float(SWEEP_Z / camera_coordinate[2]) - local_position = (ratio*camera_coordinate[0], ratio*camera_coordinate[1]) - _LOG.debug('Transfer from pixel coordinate to Camera coordinate. \n {}'.format(local_position)) + local_position = (ratio * camera_coordinate[0], ratio * camera_coordinate[1]) + _LOG.debug( + "Transfer from pixel coordinate to Camera coordinate. \n {}".format( + local_position + ) + ) return local_position -def global_coordinate(cam_coordinate: Tuple[float, float], - cam_location: Tuple[float, float, float], - cam_offset: Tuple[int, int], - gripper_offset: Tuple[int, int]) -> Tuple[float, float]: - ''' - Calculate an object's locaiton in the globale coordinate(see definition in README.md) - by coordinate transform. +def global_coordinate( + coords_camera: Tuple[float, float], + coords_encoders: Tuple[float, float, float], + offset_camera: Tuple[float, float], + offset_gripper: Tuple[float, float], +) -> Tuple[float, float]: + """Calculate an object's location in global coordinates. + + Parameters + ---------- + + coords_camera + object's centroid location in camera coordinates + coords_encoder + camera's location reading from the encoder + offset_camera + Offset between camera centroid and Z-axis of Farmbot (dx, dy) + offset_gripper + Offset between gripper and Z-axis of Farmbot (dx, dy) + + Returns + ------- + tuple of floats + x & y coordinates in global coordinate frame + """ + x_camera, y_camera = coords_camera + x_encoders, y_encoders = coords_encoders + + x_delta_camera_gripper = 150 + y_delta_camera_gripper = 15 - Input: cam_coordinate: object's centroid location in camera coordinate - cam_location: camera's location reading from the encoder <x, y, z> - cam_offset: cam_offset: distance of the camera centroid to z axis of Farmbot (dx, dy) - gripper_offset: distance of the gripper centroid to z axis of Farmbot (dx, dy) - Output: global location of a box - ''' - global_x = -cam_coordinate[1] + cam_location[0] + cam_offset[0] + gripper_offset[0] - global_y = cam_coordinate[0] + cam_location[1] + cam_offset[1] + gripper_offset[1] - return (global_x, global_y) + # TODO: Double check + x_global = x_camera + x_encoders + x_delta_camera_gripper + y_global = y_camera + y_encoders + y_delta_camera_gripper + # 411 300 45 0 + + return x_global, y_global def cal_location(args: Namespace) -> ndarray: - ''' + """ main function for this script - ''' + """ cam_offset, gripper_offset = read_offsets(args.offset) K_matrix = load_cam_matrix(args.camera_matrix) - list_location = read_locations(args.locations) - # iterate over each annotation file - _LOG.info('Global coordinate calculation begins.') - list_annotations = listdir(args.annotations) - # sort by chronological order / specific for the filename on Ziliang's PC, change if other names - list_annotations.sort() + + _LOG.info("Global coordinate calculation begins.") + + img_metadata_files = args.input_dir.glob("*.json") + # read annotations - for index_photo, annotation_file in enumerate(list_annotations): - filepath = Path(args.annotations, annotation_file) - - if not isfile(filepath): - _LOG.error('{} is not a file or does not exist'.format(annotation_file)) - return None - - if not filepath.suffix.lstrip('.') in _ANNOTATION_EXTENSIONS: - _LOG.error('{} must have an legal\ - extension: {}'.format(filepath, _ANNOTATION_EXTENSIONS)) - return None - - try: - with open(filepath, 'r') as f: - annotations = f.readlines() - except IOError: - _LOG.error('Unable to open input file {}.'.format(filepath)) - return None - _LOG.debug('Load annotation {}'.format(annotations)) - - list_global_coordinate = [] + for img_metadata_file in img_metadata_files: + with img_metadata_file.open(mode="r") as fp: + img_metadata = json.load(fp) + + annotations = img_metadata["detection_annotations"] + farmbot_metadata = img_metadata["farmbot_metadata"] + + x_encoders = farmbot_metadata["location"]["x"] + y_encoders = farmbot_metadata["location"]["y"] + + _LOG.debug(f"Loaded detection annotations: {annotations}") + + updated_annotations = [] for annotation in annotations: - detection = annotation.split() - # read the center_x center_y and class - center_x = detection[1] - center_y = detection[2] - category = detection[0] - confidence = detection[5] + x_center = annotation["x"] + y_center = annotation["y"] + # pixel coordinate to camera coordinate - local_coordinate = cam_coordinate(center_x, center_y, K_matrix) - print(local_coordinate) + x_local, y_local = cam_coordinate(x_center, y_center, K_matrix) # camera coordinate to global coordinate - global_x, global_y = global_coordinate(local_coordinate, - list_location[index_photo], cam_offset, gripper_offset) - list_global_coordinate.append([category, global_x, global_y, confidence]) - _LOG.debug(list_global_coordinate[-1]) - - _LOG.info('Global coordinate calculation is done.') - return array(list_global_coordinate) + global_x, global_y = global_coordinate( + (x_local, y_local), (x_encoders, y_encoders), cam_offset, gripper_offset + ) + annotation["global_x"] = global_x + annotation["global_y"] = global_y + updated_annotations.append(annotation) -if __name__ == '__main__': - parser = ArgumentParser(description='Transfer bounding boxes to real world coordinates') - parser.add_argument( - '-cam', - '--camera_matrix', - type=Path, - default='../static/camera_no_distortion.mat', - help='Path to mat file that contains intrinsic camera matrix K' + # the list of annotation dicts are added to the main metadata dict and + # written back to json file + img_metadata["detection_annotations"] = updated_annotations + + with img_metadata_file.open(mode="w") as fp: + json.dump(img_metadata, fp) + + +if __name__ == "__main__": + parser = ArgumentParser( + description="Transfer bounding boxes to real world coordinates" ) parser.add_argument( - '-loc', - '--locations', + "-d", + "--input_dir", type=Path, - default='../img/locations/', - help='the path to txt files contains locations from encoders corresponds to each photo' + default=TMPDIR, + help="Directory with images and metadata.", ) parser.add_argument( - '-a', - '--annotations', + "-cam", + "--camera_matrix", type=Path, - default='../img/annotations', - help='the path to txt files contains annotations for each photo' + default=REPO / "static/calibration_20220614.mat", + help="Path to mat file that contains intrinsic camera matrix K", ) parser.add_argument( - '-o', - '--offset', + "-o", + "--offset", type=Path, - default='../static/distance.txt', - help='the txt contains distance offset for camera and gripper' + default=REPO / "static/distance.txt", + help="the txt contains distance offset for camera and gripper", ) parser.add_argument( - '-l', - '--log', + "-l", + "--log", type=Path, - default='../log/location.log', - help='Path to the log file' + default=LOGDIR / "location.log", + help="Path to the log file", ) - parser.add_argument('-v', '--verbose', action='store_true', help='Verbose mode') + parser.add_argument("-v", "--verbose", action="store_true", help="Verbose mode") arguments = parser.parse_args() if arguments.verbose: @@ -261,5 +261,3 @@ if __name__ == '__main__': basicConfig(filename=arguments.log, level=INFO) cal_location(arguments) - - \ No newline at end of file diff --git a/src/farmbot_yolo/main.py b/src/farmbot_yolo/main.py index 5295b4e..e187136 100644 --- a/src/farmbot_yolo/main.py +++ b/src/farmbot_yolo/main.py @@ -1,8 +1,8 @@ -''' +""" Author: Ziliang The main script of the project, it calls scripts for movement, detection, and coordinate calculation. -''' +""" from argparse import ArgumentParser, Namespace from logging import StringTemplateStyle from os import listdir, remove @@ -19,41 +19,45 @@ from farmbot_yolo.location import * _LOG = getLogger(__name__) -GRIP_Z = 468 # measure! +GRIP_Z = 468 # measure! SCAN_Z = 0 ORIGIN_X = 0 ORIGIN_Y = 0 ORIGIN_Z = 0 -def remove_overlap(table_coordinate:DataFrame, tolerance=50.00)->DataFrame: - ''' + +def remove_overlap(table_coordinate: DataFrame, tolerance=50.00) -> DataFrame: + """ compare every two coordinates, if their Euclidean distance is smaller than tolerance , delete the one with lower probability Choose a reasonable tolerance!! :param table_coordinate: pandas dataframe that each row corresponds to a target [class, x, y, confidence] :param tolerance: a distance threshold - ''' + """ num_coordinates, num_col = table_coordinate.shape - for i in range(num_coordinates-1): - x, y, confidence = table_coordinate.loc[i, ['x','y', 'confidence']] - for j in range(i+1, num_coordinates): - x_j, y_j, confidence_j = table_coordinate.loc[j, ['x','y', 'confidence']] - distance = sqrt((float(x)-float(x_j))*(float(x)-float(x_j)) + (float(y)-float(y_j))*(float(y)-float(y_j))) - if distance <= tolerance: - if confidence < confidence_j: - table_coordinate.drop(i) - else: - table_coordinate.drop(j) - return table_coordinate + for i in range(num_coordinates - 1): + x, y, confidence = table_coordinate.loc[i, ["x", "y", "confidence"]] + for j in range(i + 1, num_coordinates): + x_j, y_j, confidence_j = table_coordinate.loc[j, ["x", "y", "confidence"]] + distance = sqrt( + (float(x) - float(x_j)) * (float(x) - float(x_j)) + + (float(y) - float(y_j)) * (float(y) - float(y_j)) + ) + if distance <= tolerance: + if confidence < confidence_j: + table_coordinate.drop(i) + else: + table_coordinate.drop(j) + return table_coordinate -def remove_temp(path: Path)-> None: - ''' +def remove_temp(path: Path) -> None: + """ Clean temporary files, i.e., photos, location.txt, annotations - ''' + """ for filename in listdir(path): - file =Path(join(path, filename)) + file = Path(join(path, filename)) if file.is_file(): remove(file) return @@ -77,11 +81,15 @@ def main(args: Namespace): list_global_coordinate = cal_location(args) _LOG.info("Global coordinate calculation is done.") # choose class - table_global_coordinate = DataFrame(list_global_coordinate, columns=['class', 'x', 'y', 'confidence']) + table_global_coordinate = DataFrame( + list_global_coordinate, columns=["class", "x", "y", "confidence"] + ) # remove overlap print(table_global_coordinate) table_global_coordinate = remove_overlap(table_global_coordinate) - goal_class = table_global_coordinate[table_global_coordinate['class']==args.category] + goal_class = table_global_coordinate[ + table_global_coordinate["class"] == args.category + ] _LOG.info("Choose {}".format(args.category)) # if there is no desiered class of plants if goal_class.empty: @@ -89,10 +97,10 @@ def main(args: Namespace): # move and grip num_goals, num_col = goal_class.shape for i in range(num_goals): - x, y = goal_class.loc[i, ['x','y']] + x, y = goal_class.loc[i, ["x", "y"]] simple_move(x, y, GRIP_Z, False) open() - gripper_open() # to make sure the gripper is open before gripping + gripper_open() # to make sure the gripper is open before gripping gripper_close() # go back to the orgin simple_move(x, y, GRIP_Z, False) @@ -100,87 +108,108 @@ def main(args: Namespace): return - - -if __name__ == '__main__': +if __name__ == "__main__": parser = ArgumentParser(description="YOLOv3 detection on Farmbot") # parsers for move parser.add_argument( - '-p', - '--photo', + "-p", + "--photo", type=Path, default="../img", - help='Mode for FarmBot, 1 for simple move with an assigned detination, 2 for scaning' + help="Mode for FarmBot, 1 for simple move with an assigned detination, 2 for scaning", ) # parsers for detect - parser.add_argument("--input", type=str, default="../img", - help="image source. It can be a single image, a" - "txt with paths to them, or a folder. Image valid" - " formats are jpg, jpeg or png." - "If no input is given, ") - parser.add_argument("--batch_size", default=1, type=int, - help="number of images to be processed at the same time") - parser.add_argument("--weights", default="../weights/yolov3-vattenhallen_best.weights", - help="yolo weights path") - parser.add_argument("--ext_output", action='store_true', default=True, - help="display bbox coordinates of detected objects") - parser.add_argument("--save_labels", action='store_true', default=True, - help="save detections bbox for each image in yolo format") - parser.add_argument("--config_file", default="../cfg/yolov3-vattenhallen-test.cfg", - help="path to config file") - parser.add_argument("--data_file", default="../data/vattenhallen.data", - help="path to data file") - parser.add_argument("--thresh", type=float, default=.25, - help="remove detections with lower confidence") + parser.add_argument( + "--input", + type=str, + default="../img", + help="image source. It can be a single image, a" + "txt with paths to them, or a folder. Image valid" + " formats are jpg, jpeg or png." + "If no input is given, ", + ) + parser.add_argument( + "--batch_size", + default=1, + type=int, + help="number of images to be processed at the same time", + ) + parser.add_argument( + "--weights", + default="../weights/yolov3-vattenhallen_best.weights", + help="yolo weights path", + ) + parser.add_argument( + "--ext_output", + action="store_true", + default=True, + help="display bbox coordinates of detected objects", + ) + parser.add_argument( + "--save_labels", + action="store_true", + default=True, + help="save detections bbox for each image in yolo format", + ) + parser.add_argument( + "--config_file", + default="../cfg/yolov3-vattenhallen-test.cfg", + help="path to config file", + ) + parser.add_argument( + "--data_file", default="../data/vattenhallen.data", help="path to data file" + ) + parser.add_argument( + "--thresh", + type=float, + default=0.25, + help="remove detections with lower confidence", + ) # arguemtns for grip parser.add_argument( - '-ca', - '--category', + "-ca", + "--category", type=int, - help='Choose the class of fruits to be picked up. There are tomato, mushroom,\ - potato, carrot, beetroot, zucchini, hand' + help="Choose the class of fruits to be picked up. There are tomato, mushroom,\ + potato, carrot, beetroot, zucchini, hand", ) # arguments for location parser.add_argument( - '-cam', - '--camera_matrix', + "-cam", + "--camera_matrix", type=Path, - default='../static/camera_no_distortion.mat', - help='Path to mat file that contains intrinsic camera matrix K' + default="../static/camera_no_distortion.mat", + help="Path to mat file that contains intrinsic camera matrix K", ) parser.add_argument( - '-loc', - '--locations', + "-loc", + "--locations", type=Path, - default='../img/locations/', - help='the path to txt files contains locations from encoders corresponds to each photo' + default="../img/locations/", + help="the path to txt files contains locations from encoders corresponds to each photo", ) parser.add_argument( - '-a', - '--annotations', + "-a", + "--annotations", type=Path, - default='../img/annotations', - help='the path to txt files contains annotations for each photo' + default="../img/annotations", + help="the path to txt files contains annotations for each photo", ) parser.add_argument( - '-o', - '--offset', + "-o", + "--offset", type=Path, - default='../static/distance.txt', - help='the txt contains distance offset for camera and gripper' + default="../static/distance.txt", + help="the txt contains distance offset for camera and gripper", ) parser.add_argument( - '-l', - '--log', - type=Path, - default='../log/main.log', - help='Path to the log file' - ) - parser.add_argument('-v', '--verbose', action='store_true', help='Verbose mode.') - arguments = parser.parse_args() + "-l", "--log", type=Path, default="../log/main.log", help="Path to the log file" + ) + parser.add_argument("-v", "--verbose", action="store_true", help="Verbose mode.") + args = parser.parse_args() - if arguments.verbose: - basicConfig(filename=arguments.log, level=DEBUG) + if args.verbose: + basicConfig(filename=args.log, level=DEBUG) else: - basicConfig(filename=arguments.log, level=INFO) - main(arguments) + basicConfig(filename=args.log, level=INFO) + main(args) diff --git a/src/farmbot_yolo/move.py b/src/farmbot_yolo/move.py index 064cf55..f44058d 100644 --- a/src/farmbot_yolo/move.py +++ b/src/farmbot_yolo/move.py @@ -21,37 +21,27 @@ from urllib import request from datetime import timezone, datetime from dateutil.parser import parse from requests import get, delete +from farmbot_yolo import LOGDIR, TMPDIR -import creds -from client import FarmbotClient +from farmbot_yolo import creds +import farmbot_yolo +from farmbot_yolo.client import FarmbotClient +from farmbot_yolo.download import download_images _SWEEEP_HEIGHT = 0 -Logger = getLogger(__name__) - - -class Opts: - def __init__(self, min_x, max_x, min_y, max_y, delta, offset, flag): - self.min_x = min_x - self.max_x = max_x - self.min_y = min_y - self.max_y = max_y - self.delta = delta - self.offset = offset - self.flag = flag +log = getLogger(__name__) def scan( - img_path: Path, - location_path: Path, # smaller delta + img_dir: Path, min_x=0, max_x=1175, min_y=0, max_y=974, delta=300, offset=0, - flag=True, -) -> List: # 里面的数字需要重新测量 +) -> List: """ scan the bed at a certain height, first move along x axis, then y, like a zig zag; Taking pictures and record the location of the camera that corresponds to the picture @@ -62,57 +52,28 @@ def scan( max_y: back most point on y axis delta: the interval for scaning offset: - flag: for degging, if true, don't actually drive FarmBot - Output: none """ - opts = Opts(min_x, max_x, min_y, max_y, delta, offset, flag) - creds = read_credentials() - pts = [] sweep_y_negative = False - for x in range(opts.min_x, opts.max_x, opts.delta): - y_range = range(opts.min_y, opts.max_y, opts.delta) + for x in range(min_x, max_x, delta): + y_range = range(min_y, max_y, delta) if sweep_y_negative: y_range = reversed(y_range) sweep_y_negative = not sweep_y_negative for y in y_range: - pts.append((x + opts.offset, y + opts.offset)) - - Logger.info("Moving pattern generated") + pts.append((x + offset, y + offset)) - if opts.flag: - Logger.info("Run without sweep") - exit() + log.info("Moving pattern generated") - client = FarmbotClient(creds["device_id"], creds["token"]) + client = FarmbotClient(creds.device_id, creds.token) client.move(0, 0, _SWEEEP_HEIGHT) # ensure moving from original for x, y in pts: client.move(x, y, _SWEEEP_HEIGHT) # move camera # take_photo(img_path) client.take_photo() client.shutdown() - # write to img/location - with open(path.join(location_path, "location.txt"), "w") as f: - for postion in pts: - f.write("{} {} {}\n".format(postion[0], postion[1], _SWEEEP_HEIGHT)) - return None - -def take_photo(): - client = FarmbotClient(creds.device_id, creds.token) - client.take_photo() - # download image - system("python ./utils/download.py") - - -# def take_photo(img_path: Path): -# HERE = path.dirname(__file__) -# IMG_DIR = path.join(HERE, img_path) - -# with request.urlopen('http://localhost:8080/?action=snapshot') as photo: -# filename = datetime.now().strftime("%Y-%m-%dT%H:%M:%S") + ".jpg" -# with open(path.join(IMG_DIR, filename), mode="wb") as save_file: -# save_file.write(photo.read()) + return pts def simple_move(x: int, y: int, z: int) -> None: @@ -134,40 +95,35 @@ if __name__ == "__main__": "-m", "--mode", type=int, - help="Mode for FarmBot, 1 for simple move with an assigned detination, 2 for scaning", + help="Mode for FarmBot, 1 for simple move with an assigned detination, 2 for scanning", ) parser.add_argument( - "-l", "--log", type=Path, default="../log/move.log", help="Path to the log file" - ) - parser.add_argument( - "-p", - "--photo", + "-l", + "--log", type=Path, - default="../img", - help="Mode for FarmBot, 1 for simple move with an assigned detination, 2 for scaning", + default=LOGDIR / "move.log", + help="Path to the log file", ) parser.add_argument( - "-loc", - "--locations", + "-p", + "--photo_dir", type=Path, - default="../img/locations/", - help="the path to txt files contains locations from encoders corresponds to each photo", + default=TMPDIR, + help="Directory to store photos.", ) parser.add_argument("-v", "--verbose", action="store_true", help="Verbose mode") - arguments = parser.parse_args() + args = parser.parse_args() - if arguments.mode == 1: - Logger.info("Input the destination:") + if args.mode == 1: + log.info("Input the destination:") destination_x = int(input("X:")) destination_y = int(input("Y:")) destination_z = int(input("Z:")) simple_move_start = time() simple_move(destination_x, destination_y, destination_z) - Logger.info(f"time cost {time()-simple_move_start}") - elif arguments.mode == 2: - scan(arguments.photo, arguments.locations, flag=False) - # take_photo(arguments.photo) - elif arguments.mode == 3: - take_photo() + log.info(f"time cost {time()-simple_move_start}") + elif args.mode == 2: + scan(args.photo_dir) + download_images(args.photo_dir) else: - Logger.error("Wrong mode number {arguments.mode}") + log.error("Wrong mode number {arguments.mode}") diff --git a/src/farmbot_yolo/try.py b/src/farmbot_yolo/try.py deleted file mode 100644 index 9f42d8e..0000000 --- a/src/farmbot_yolo/try.py +++ /dev/null @@ -1,14 +0,0 @@ -from os import listdir, remove -from os.path import join -from pathlib import Path - -def remove_temp(path: Path)-> None: - # list file - for filename in listdir(path): - file =Path(join(path, filename)) - if file.is_file(): - remove(file) - return - -path = '../img' -remove_temp(path) \ No newline at end of file diff --git a/static/distance.txt b/static/distance.txt index 79999e8..ec6f0de 100644 --- a/static/distance.txt +++ b/static/distance.txt @@ -1,6 +1,6 @@ camera's distance to the encoder -0 -0 +-30 +-130 gripper's distance to the encoder +45 0 -0 \ No newline at end of file -- GitLab