From 46fc472a40fdcda56edef7a858a0d6a9fb4418f2 Mon Sep 17 00:00:00 2001 From: heyarne Date: Thu, 3 Jun 2021 19:27:18 +0200 Subject: [PATCH] Copy over all line-us plotting code from https://github.com/heyarne/line-us/tree/fa9dbf212049cbc0bef1aca868f9e329bf18a592 There was a problem with origami which should probably be solved by narrowing the focus of the git Line-us git repository --- src/heyarne/line_us/connection.clj | 73 ++++++++++++++++++++++++ src/heyarne/line_us/edge_detection.clj | 79 ++++++++++++++++++++++++++ src/heyarne/line_us/gcode.clj | 48 ++++++++++++++++ src/heyarne/line_us/helpers.clj | 34 +++++++++++ 4 files changed, 234 insertions(+) create mode 100644 src/heyarne/line_us/connection.clj create mode 100644 src/heyarne/line_us/edge_detection.clj create mode 100644 src/heyarne/line_us/gcode.clj create mode 100644 src/heyarne/line_us/helpers.clj diff --git a/src/heyarne/line_us/connection.clj b/src/heyarne/line_us/connection.clj new file mode 100644 index 0000000..30cb65f --- /dev/null +++ b/src/heyarne/line_us/connection.clj @@ -0,0 +1,73 @@ +(ns heyarne.line-us.connection + (:require [clojure.java.io :as io]) + (:import [java.net Socket])) + +;; https://github.com/Line-us/Line-us-Programming/blob/master/Documentation/LineUsDrawingArea.pdf +(def drawing-area + {:x [800 1700] + :y [-900 900] + :z [0 1000]}) + +(defn connect + [url port] + (Socket. url port)) + +(defn disconnect [^Socket line-us] + (.close line-us)) + +(defn read-response [^Socket line-us] + (let [stream (io/input-stream line-us)] + (loop [line []] + (let [c (.read stream)] + (case c + 0 (when-not (empty? line) + (apply str (map char line))) + -1 (recur line) + (recur (conj line c))))))) + +(defmacro validate-coord + "Throws an exception when a coordinate is outside of the drawable area" + [coord] + (let [[min-v# max-v#] (get drawing-area (keyword (name coord)))] + `(when-not (< ~(dec min-v#) ~coord ~(inc max-v#)) + (throw (IllegalArgumentException. + (str ~(name coord) " should be in range [" ~min-v# ", " ~max-v# "] but is " ~coord)))))) + +(defn- send-command! [^Socket line-us ^String raw-cmd] + ;; this is basically taken from the Processing example code and the processing + ;; "Client" class + (doto (io/output-stream line-us) + (.write (.getBytes (str raw-cmd "\0"))) + (.flush)) + ;; wait for the response + (let [res (read-response line-us)] + (if-not (re-find #"^(ok|hello)" res) + (throw (Exception. res)) + res))) + +(defn send-movement! + "Moves the arm to the [x y z] vector that is coord. Coordinates are validated + to be inside the valid drawing area." + [^Socket line-us [x y z :as coords]] + (validate-coord x) + (validate-coord y) + (validate-coord z) + (send-command! line-us (str "G01 X" x " Y" y " Z" z)) + coords) + +(defn move-home! [^Socket line-us] + (send-movement! line-us [1000 1000 1000])) + +(defn- parse-coords [^String raw-coords] + (->> + (re-find #"X:(-?\d+) Y:(\d+) Z:(\d+)" raw-coords) + (rest) + (mapv #(Integer/parseInt % 10)))) + +(defn move-up! [^Socket line-us] + (let [[x y _] (parse-coords (send-command! line-us "M114"))] + (send-movement! line-us [x y 1000]))) + +(defn move-down! [^Socket line-us] + (let [[x y _] (parse-coords (send-command! line-us "M114"))] + (send-movement! line-us [x y 200]))) diff --git a/src/heyarne/line_us/edge_detection.clj b/src/heyarne/line_us/edge_detection.clj new file mode 100644 index 0000000..54657a3 --- /dev/null +++ b/src/heyarne/line_us/edge_detection.clj @@ -0,0 +1,79 @@ +(ns heyarne.line-us.edge-detection + (:require [clojure.set :as set] + [opencv4.core :as cv] + [opencv4.colors.rgb :as rgb] + [opencv4.utils :as u]) + (:import [org.opencv.core Point Rect])) + +(def img (-> (u/mat-from-url "https://raw.githubusercontent.com/hellonico/origami/master/doc/cat_in_bowl.jpeg") + (cv/cvt-color! cv/COLOR_RGB2GRAY) + (u/resize-by 0.2) + (cv/canny! 300.0 100.0 3 true) + (cv/bitwise-not!))) ;; NOTE: The bitwise-not! here is not actually needed + +(defn pixels + "Returns a vector of + - all pixels in an org.opencv.core.Mat as a byte-array + - width + - height" + [mat] + (let [out (byte-array (* (.total mat) (.channels mat)))] + (.get mat 0 0 out) + [out (.cols mat) (.rows mat)])) + +(defn neighborhood [pixels width height x y] + (for [y-off [-1 0 1] + x-off [-1 0 1] + :let [y' (+ y y-off) + x' (+ x x-off)] + :when (and (< -1 x' width) + (< -1 y' height) + (not= y-off x-off 0))] + [[x' y'] (nth pixels (+ x' (* y' width)))])) + +(def shape? zero?) + +(defn next-connection + "Returns the first neighbor of a pixel that's part of the same shape" + [pixels width height contour coord] + (let [neighbors (apply neighborhood pixels width height coord)] + (reduce (fn [_ [neighbor-coord v]] + (when (and (shape? v) + (not (contour neighbor-coord))) + (reduced neighbor-coord))) neighbors))) + +(defn next-start [contour-pixels found-contours] + (first (remove (apply set/union found-contours) contour-pixels))) + +(defn radial-sweep + "Returns a seq of sets, where each set represents one stroke / contour" + [mat] + (let [[pixels width height] (pixels mat) + contour-pixels (for [idx (range (.total mat)) + :when (shape? (nth pixels idx))] + [(rem idx width) (quot idx width)]) + start (first contour-pixels)] + (loop [coord start + contour #{start} + found-contours []] + (let [next (next-connection pixels width height contour coord)] + (if (nil? next) + contour + ;; we're continuing with our current contour + (recur next (conj contour next) found-contours)))))) + +(defn draw-coords! [mat [x y]] + (cv/circle mat (cv/new-point x y) 1 rgb/green 0 cv/LINE_AA) + mat) + + +(comment + (u/show img) + + (radial-sweep img) + + (let [out (-> (cv/clone img) + (cv/cvt-color! cv/COLOR_GRAY2RGB))] + (doseq [xy (radial-sweep img)] + (draw-coords! out xy)) + (u/show out {:frame {:width (.cols out) :height (.rows out)}}))) diff --git a/src/heyarne/line_us/gcode.clj b/src/heyarne/line_us/gcode.clj new file mode 100644 index 0000000..413a35e --- /dev/null +++ b/src/heyarne/line_us/gcode.clj @@ -0,0 +1,48 @@ +(ns heyarne.line-us.gcode + "Provides functions to move from geometry types provided by thi.ng to gcode + so you can plot them." + (:require [thi.ng.geom.core :as g]) + (:import [thi.ng.geom.types Line2 LineStrip2 Circle2])) + +(defprotocol GCode + "Convert thi.ng.geom types into sequences of GCode to plot them. The GCode + instructions are given as a sequence of vectors, where each vector is given + as [x y z]." + (->gcode [_] [_ r] + "Returns G01 movements on the x, y, and z axis. Z is constantly 1000 in the + current implementation, but this might change in the future. The optional + parameter `r` can be used to adjust the resolution.")) + +(defn- point-seq->gcode [pts] + (let [vertices pts + [l-x l-y] (last vertices)] + (conj (mapv (fn [[x y]] + [x y 0]) vertices) + [l-x l-y 1000]))) + +(defn gcode-seq->str [gcode-seq] + (map (fn [[x y z]] + (str "G01 X" x " Y" y " Z" z)) gcode-seq)) + +(extend-protocol GCode + Line2 + (->gcode + ([_] (point-seq->gcode (:points _))) + ([_ r] (->gcode _))) + + LineStrip2 + (->gcode + ([_] (point-seq->gcode (:points _))) + ([_ r] (->gcode _))) + + Circle2 + (->gcode + ([_] (point-seq->gcode (:points (g/as-polygon _)))) + ([_ r] (point-seq->gcode (:points (g/as-polygon _)))))) + +(comment + (require '[thi.ng.geom.circle :as c]) + + (-> + (->gcode (c/circle)) + (gcode-seq->str))) diff --git a/src/heyarne/line_us/helpers.clj b/src/heyarne/line_us/helpers.clj new file mode 100644 index 0000000..0ad2376 --- /dev/null +++ b/src/heyarne/line_us/helpers.clj @@ -0,0 +1,34 @@ +(ns heyarne.line-us.helpers + (:require [thi.ng.geom.rect :as rect] + [thi.ng.math.core :as m])) + +(defn g01-bounds + "Returns a rect representing the drawing bounds of a sequence of g01 coords" + [g01] + (let [bounds [Double/MAX_VALUE Double/MIN_VALUE Double/MIN_VALUE Double/MAX_VALUE] + [top right bottom left] (reduce (fn [[top right bottom left] [x y _]] + [(min top y) (max right x) + (max bottom y) (min left x)]) + bounds + g01)] + (rect/rect [left bottom] [right top]))) + +(defn rescale + "Returns a new g01 sequence that is proportionally scaled to fit into the + bounding box passed in as the second argument. Assumes that the top left + in g01-seq is at [0 0]." + [g01-seq [top right bottom left]] + (let [s-bounds (g01-bounds g01-seq) + t-bounds (rect/rect [left bottom] [right top]) + ;; we need to translate the bounding box + [translate-x translate-y] (m/+ (:p s-bounds) (:p t-bounds)) + ;; and scale it + [s-x s-y] (:p s-bounds) + [t-x t-y] (:p t-bounds) + [s-width s-height] (:size s-bounds) + [t-width t-height] (:size t-bounds) + factor (min (/ (+ t-x t-width) (+ s-x s-width)) (/ (+ t-y t-height) (+ s-y s-height)))] + (map (fn [coord] + (-> (update coord 0 #(+ translate-x (* % factor))) + (update 1 #(+ translate-y (* % factor))))) + g01-seq)))