Mturk Drawing with padding

This commit is contained in:
Ruben van de Ven 2019-10-31 13:55:22 +01:00
parent a43437188b
commit 1926e2be24
13 changed files with 334 additions and 133 deletions

View file

@ -3,11 +3,25 @@ amazon:
user_secret: "213j234/234sksjdfus83jd" user_secret: "213j234/234sksjdfus83jd"
mturk_sandbox: true mturk_sandbox: true
mturk_region: us-east-1 mturk_region: us-east-1
sqs_endpoint_url: "https://sqs.eu-west-3.amazonaws.com/"
sqs_url: "https://sqs.eu-west-3.amazonaws.com/60123456789/your_queue" sqs_url: "https://sqs.eu-west-3.amazonaws.com/60123456789/your_queue"
sqs_region_name: "eu-west-3" sqs_region_name: "eu-west-3"
task_xml: "mt_task.xml" task_xml: "mt_task.xml"
hit_db: store.db hit_db: store.db
hour_rate_aim: 15 hour_rate_aim: 15
hit_lifetime: 54000 ;15*60*60 hit_lifetime: 54000 #15*60*60
hit_assignment_duration: 300 ; 5*60 hit_assignment_duration: 300 # 5*60
hit_autoapprove_delay: 3600 hit_autoapprove_delay: 3600
dummy_plotter: false
server:
port: 8888
scanner: # size of scanarea in mm
# total visible glass (and size of the scan)
width: 255
height: 185
# area which can be removed
draw_width: 255
draw_height: 70
# part of scanner that is invissible left & top
left_padding: 0
top_padding: 45

View file

@ -61,7 +61,7 @@ class HIT(Base):
return os.path.join('www', self.getImageUrl()) return os.path.join('www', self.getImageUrl())
def getImageUrl(self): def getImageUrl(self):
return f"scans/{self.id}.png" return f"scans/{self.id}.jpg"
def getStatus(self): def getStatus(self):
if self.scanned_at: if self.scanned_at:

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -300,8 +300,11 @@ class CentralManagement():
Run scanimage on scaner and returns a string with the filename Run scanimage on scaner and returns a string with the filename
""" """
self.eventQueue.put(Signal('scan.start')) self.eventQueue.put(Signal('scan.start'))
cmd = [ cmd = [
'sudo', 'scanimage', '-d', 'epkowa' 'sudo', 'scanimage', '-d', 'epkowa', '--format', 'jpeg',
'--resolution=100', '-l','20','-t','30','-x',(self.config['scanner']['height']),
'-y',str(self.config['scanner']['width'])
] ]
filename = self.currentHit.getImagePath() filename = self.currentHit.getImagePath()
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
@ -313,6 +316,7 @@ class CentralManagement():
f = io.BytesIO(o) f = io.BytesIO(o)
img = Image.open(f) img = Image.open(f)
img = img.transpose(Image.ROTATE_90)
img.save(filename) img.save(filename)
self.eventQueue.put(Signal('hit.scanned', {'hit_id':self.currentHit.id})) self.eventQueue.put(Signal('hit.scanned', {'hit_id':self.currentHit.id}))

View file

@ -6,17 +6,6 @@ from threading import Event
from sorteerhoed.Signal import Signal from sorteerhoed.Signal import Signal
import time import time
class PathSegment:
def __init__(self):
self.d = []
def add(self, i):
# self.d.append(i)
def get(self, prop):
if prop =='d':
return self.d
class Plotter: class Plotter:
def __init__(self, config, eventQ: Queue, runningEvent: Event): def __init__(self, config, eventQ: Queue, runningEvent: Event):
#TODO: scanningEvent -> CentralManagement.isScanning -> prevent plotter move during scan, failsafe #TODO: scanningEvent -> CentralManagement.isScanning -> prevent plotter move during scan, failsafe
@ -25,6 +14,13 @@ class Plotter:
self.q = Queue() self.q = Queue()
self.isRunning = runningEvent self.isRunning = runningEvent
self.logger = logging.getLogger("sorteerhoed").getChild("plotter") self.logger = logging.getLogger("sorteerhoed").getChild("plotter")
self.pen_down = False
self.plotWidth = self.config['scanner']['width'] / 10 / 2.54
self.plotHeight = self.config['scanner']['height'] / 10 / 2.54
self.xPadding = self.config['scanner']['left_padding'] / 10 / 2.54;
self.yPadding = self.config['scanner']['top_padding'] / 10 / 2.54;
self.logger.info(f"Paddings x: {self.xPadding} inch y: {self.yPadding} inch")
def park(self): def park(self):
self.logger.info("Queue to park plotter") self.logger.info("Queue to park plotter")
@ -37,69 +33,118 @@ class Plotter:
self.q.put([0,0,0]) self.q.put([0,0,0])
def start(self): def start(self):
self.axiDrawCueListener() try:
if not self.config['dummy_plotter']:
def axiDrawCueListener(self): self.ad = axidraw.AxiDraw()
if self.config['dummy_plotter']:
while self.isRunning.is_set(): self.ad.interactive()
# TODO: queue that collects a part of the path data # self.ad.plot_path()
# on a specific limit or on a specific time interval, do the plot
# also, changing ad.pen_raise() or ad.pen_lower() trigger a new segment connected = self.ad.connect()
# Plot with ad.plan_trajectory() ?? if not connected:
plotterRan = False raise Exception("Cannot connect to Axidraw")
try:
move = self.q.get(True, 1)
plotterRan = True
except queue.Empty as e:
self.logger.log(5, "Empty queue.")
if plotterRan:
plotterRan = False
self.eventQ.put(Signal('plotter.finished'))
else:
time.sleep(.05)
self.logger.debug(f'Dummy plotter move: {move}')
self.logger.info("Stopping dummy plotter")
else:
self.ad = axidraw.AxiDraw()
self.ad.interactive()
# self.ad.plot_path()
connected = self.ad.connect()
if not connected:
raise Exception("Cannot connect to Axidraw")
try:
self.ad.options.units = 1 # set to use centimeters instead of inches self.ad.options.units = 1 # set to use centimeters instead of inches
self.ad.options.accel = 100; self.ad.options.accel = 100;
self.ad.options.speed_penup = 100 self.ad.options.speed_penup = 100
self.ad.options.speed_pendown = 100 self.ad.options.speed_pendown = 100
self.ad.options.model = 1 # A3, set to 1 for A4 self.ad.options.model = 1 # 2 for A3, 1 for A4
self.ad.moveto(0,0) self.ad.moveto(0,0)
plotterWidth = 22 # plotterWidth = 25
plotterHeight = 18 # 16? # plotterHeight = 21
plotterRan = False else:
while self.isRunning.is_set(): self.ad = None
# TODO: set timeout on .get() with catch block, so we can escape if no moves come in self.axiDrawCueListener()
try: except Exception as e:
move = self.q.get(True, 1) self.logger.exception(e)
plotterRan = True finally:
except queue.Empty as e: self.logger.warning("Close Axidraw connection")
self.logger.log(5, "Empty queue.") if self.ad:
if plotterRan:
plotterRan = False
self.eventQ.put(Signal('plotter.finished'))
else:
self.ad.moveto(move[0]* plotterWidth, move[1]*plotterHeight)
self.logger.debug(f'handler! {move}')
except Exception as e:
self.logger.exception(e)
finally:
self.logger.warning("Close Axidraw connection")
self.ad.moveto(0,0) self.ad.moveto(0,0)
self.ad.disconnect() self.ad.disconnect()
# send shutdown signal (if not already set)
self.isRunning.clear()
def draw_segments(self, segments = []):
coordinates = []
for segment in segments:
coordinate = [
# mm to cm to inches
(segment[0]) * self.plotWidth,
(1-segment[1]) * self.plotHeight
]
#prevent drawing when not in drwaing area
# this is a failsafe for a malicious working or glitching script, as this should also be done in the javascript
if self.pen_down:
if coordinate[0] < self.xPadding or coordinate[0] > self.xPadding+self.plotWidth or \
coordinate[1] < self.yPadding or coordinate[1] > self.yPadding + self.plotHeight:
self.logger.warn(f"Skip drawing for: {coordinates} out of bounds")
continue
coordinates.append(coordinate)
self.logger.info(f"Plot: {coordinates}")
if self.ad:
self.ad.plan_trajectory(coordinates)
# self.ad.moveto(move[0]* plotterWidth, move[1]*plotterHeight)
# self.logger.debug(f'handler! {move}')
pass
def setPenDown(self, pen_state):
"""
False: pen raised, True: pen_lower
"""
if pen_state != self.pen_down:
self.pen_down = pen_state
self.logger.info("Changed pen: {}".format('down' if pen_state else 'up'))
if self.ad:
if pen_state:
self.ad.pen_lower()
else:
self.ad.pen_raise()
return True
return False
def axiDrawCueListener(self):
plotterRan = False
segments = []
while self.isRunning.is_set():
# TODO: queue that collects a part of the path data
# on a specific limit or on a specific time interval, do the plot
# also, changing ad.pen_raise() or ad.pen_lower() trigger a new segment
# Plot with ad.plan_trajectory() ??
try:
# if no info comes in for .5sec, plot that segment
segment = self.q.get(True, .5)
# send shutdown signal (if not already set) # change of pen state? draw previous segments!
self.isRunning.clear() if (segment[2] == 1 and not self.pen_down) or (segment[2] == 0 and self.pen_down) or len(segments) > 150:
if len(segments) > 1:
self.draw_segments(segments)
plotterRan = True
segments = [] #reset
# and change pen positions
self.setPenDown(segment[2] == 1)
segments.append(segment)
except queue.Empty as e:
self.logger.debug("Timeout queue.")
if len(segments):
# segments to plot!
self.draw_segments(segments)
plotterRan = True
segments = []
elif plotterRan:
plotterRan = False
self.eventQ.put(Signal('plotter.finished'))
# else:
# time.sleep(.05)
# self.logger.debug(f'Plotter move: {move}')
self.logger.info("Stopping plotter")

View file

@ -233,9 +233,15 @@ def strokes2D(strokes):
return d; return d;
class DrawPageHandler(tornado.web.RequestHandler): class DrawPageHandler(tornado.web.RequestHandler):
def initialize(self, store: HITStore, path: str): def initialize(self, store: HITStore, path: str, width: int, height: int, draw_width: int, draw_height: int, top_padding: int, left_padding: int):
self.store = store self.store = store
self.path = path self.path = path
self.width = width
self.height = height
self.draw_width = draw_width
self.draw_height = draw_height
self.top_padding = top_padding
self.left_padding = left_padding
def get(self): def get(self):
try: try:
@ -258,7 +264,14 @@ class DrawPageHandler(tornado.web.RequestHandler):
logger.info(f"Image url: {image}") logger.info(f"Image url: {image}")
self.set_header("Access-Control-Allow-Origin", "*") self.set_header("Access-Control-Allow-Origin", "*")
contents = open(os.path.join(self.path, 'index.html'), 'r').read().replace("{IMAGE_URL}", image) contents = open(os.path.join(self.path, 'index.html'), 'r').read()
contents = contents.replace("{IMAGE_URL}", image)\
.replace("{WIDTH}", str(self.width))\
.replace("{HEIGHT}", str(self.height))\
.replace("{DRAW_WIDTH}", str(self.draw_width))\
.replace("{DRAW_HEIGHT}", str(self.draw_height))\
.replace("{TOP_PADDING}", str(self.left_padding))\
.replace("{LEFT_PADDING}", str(self.top_padding))
self.write(contents) self.write(contents)
class StatusPage(): class StatusPage():
@ -342,7 +355,16 @@ class Server:
}), }),
(r"/status/ws", StatusWebSocketHandler), (r"/status/ws", StatusWebSocketHandler),
(r"/draw", DrawPageHandler, (r"/draw", DrawPageHandler,
dict(store = self.store, path=self.web_root)), dict(
store = self.store,
path=self.web_root,
width=self.config['scanner']['width'],
height=self.config['scanner']['height'],
draw_width=self.config['scanner']['draw_width'],
draw_height=self.config['scanner']['draw_height'],
top_padding=self.config['scanner']['top_padding'],
left_padding=self.config['scanner']['left_padding']
)),
(r"/(.*)", StaticFileWithHeaderHandler, (r"/(.*)", StaticFileWithHeaderHandler,
{"path": self.web_root}), {"path": self.web_root}),
], debug=True, autoreload=False) ], debug=True, autoreload=False)

View file

@ -12,6 +12,7 @@
right:0; right:0;
width:100%; width:100%;
height:100%; height:100%;
font-family: sans-serif;
} }
path { path {
@ -19,7 +20,6 @@
stroke: red; stroke: red;
stroke-width: 2px; stroke-width: 2px;
} }
body.submitted path{ body.submitted path{
stroke:darkgray; stroke:darkgray;
} }
@ -28,38 +28,121 @@
display:none; display:none;
} }
#wrapper { /*#wrapper {
height: 600px; height: calc({DRAW_HEIGHT}/{HEIGHT} * 100%);
width: 600px; width: calc({DRAW_WIDTH}/{WIDTH} * 100%);
position: relative; position: absolute;
background:#ccc; left: calc(({WIDTH} - {DRAW_WIDTH})/2/{WIDTH} * 100%);
top: calc(({HEIGHT} - {DRAW_HEIGHT})/2/{HEIGHT} * 100%);
background:none;
cursor: url(cursor.png) 6 6, auto; cursor: url(cursor.png) 6 6, auto;
}*/
#wrapper {
position:absolute;
top:0;
right:0;
bottom:0;
left:0;
background:none;
cursor: url(cursor.png) 6 6, auto;
}
.gray{
position:absolute;
background:rgba(255,255,255,0.7);
}
#gray_top{
left:0;
right:0;
top:0;
height:calc({TOP_PADDING}/{HEIGHT} * 100%);
}
#gray_bottom{
left:0;
right:0;
bottom:0;
height:calc(({HEIGHT} - {DRAW_HEIGHT} - {TOP_PADDING})/{HEIGHT} * 100%);
}
#gray_left{
left:0;
top:calc({TOP_PADDING}/{HEIGHT} * 100%);
height: calc({DRAW_HEIGHT}/{HEIGHT} * 100%);
width: calc({LEFT_PADDING}/{WIDTH} * 100%);
}
#gray_right{
right:0;
top:calc({TOP_PADDING}/{HEIGHT} * 100%);
height: calc({DRAW_HEIGHT}/{HEIGHT} * 100%);
width: calc(({WIDTH} - {DRAW_WIDTH} - {LEFT_PADDING})/{WIDTH} * 100%);
} }
html, body{ html, body{
height: 100%; height: 100%;
width: 100%; width: 100%;
margin:0;
background:gray;
} }
#interface{
background:white;
height: 0;
overflow: hidden;
padding-top: calc({HEIGHT}/{WIDTH} * 100%);
background: white;
position: relative;
margin: 0 auto;
background-size: 100% 100%;
}
#innerface{
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
@media (min-aspect-ratio: {WIDTH}/{HEIGHT}) {
#interface {
height: 100vh;
width: calc({WIDTH}/{HEIGHT} * 100vh);
padding-top:0;
}
}
#info{
position: absolute;
bottom: 5px;
width: 600px;
left: calc(50% - 250px);
z-index: 999;
}
.buttons{
text-align: center;
}
</style> </style>
</head> </head>
<body> <body>
<ul> <div id='interface' style="background-image:url('{IMAGE_URL}')">
<li>Drag the mouse to trace the line drawing</li> <div id='innerface'>
<li>Follow the lines as precise as possible</li> <div id='wrapper'>
<li>Press submit when you're done.</li> <!-- <img src="{IMAGE_URL}" id='sample'>-->
<li>You'll receive a submission token, to fill in at Mechanical Turk</li> <svg id="canvas" viewBox="0 0 {WIDTH}0 {HEIGHT}0" width="{WIDTH}mm" height="{HEIGHT}mm" preserveAspectRatio="none">
</ul> <path d="" id="stroke" />
<div id='interface'> </svg>
<div id='wrapper'> </div>
<img src="{IMAGE_URL}" id='sample'> <div id='info'>
<svg id="canvas"> <ul>
<path d="" id="stroke" /> <li>Drag the mouse to trace the <strong>clearest lines</strong> drawing above</li>
</svg> <li>Follow the lines as precise as possible</li>
</div> <li>Press submit when you're done.</li>
<div class='buttons'> <li>You'll receive a submission token, to fill in at Mechanical Turk</li>
<button id='submit'>Submit</button> </ul>
<button id='reset'>Reset</button> <div class='buttons'>
</div> <button id='submit'>Submit</button>
<div id='message'></div> <!-- <button id='reset'>Reset</button>-->
</div>
<div id='message'></div>
</div>
<div class='gray' id='gray_top'></div>
<div class='gray' id='gray_bottom'></div>
<div class='gray' id='gray_left'></div>
<div class='gray' id='gray_right'></div>
</div>
</div> </div>
<script type="text/javascript"> <script type="text/javascript">
let url = window.location.origin.replace('http', 'ws') +'/ws?' + window.location.search.substring(1); let url = window.location.origin.replace('http', 'ws') +'/ws?' + window.location.search.substring(1);
@ -68,50 +151,77 @@
let submitEl = document.getElementById('submit'); let submitEl = document.getElementById('submit');
let resetEl = document.getElementById('reset'); let resetEl = document.getElementById('reset');
let messageEl = document.getElementById('message'); let messageEl = document.getElementById('message');
let innerFaceEl = document.getElementById('innerface'); // wrapper within the interface
let svgWidth = {WIDTH};
let svgHeight = {HEIGHT};
let drawWidth = {DRAW_WIDTH};
let drawHeight = {DRAW_HEIGHT};
let xPadding = {LEFT_PADDING} / svgWidth;
let yPadding = {TOP_PADDING} / svgHeight;
let drawWidthFactor = drawWidth / svgWidth;
let drawHeightFactor = drawHeight / svgHeight;
let strokes = []; let strokes = [];
let isDrawing = false; let isDrawing = false;
let hasMouseDown = false;
let currentPoint = null; let currentPoint = null;
let getCoordinates = function(e) {
// convert event coordinates into relative positions on x & y axis
let box = innerFaceEl.getBoundingClientRect();
let x = (e.x - box['left']) / box['width'];
let y = (e.y - box['top']) / box['height'];
return {'x': x, 'y': y};
}
let isInsideBounds = function(pos) {
return !(pos['x'] < 0 || pos['y'] < 0 || pos['x'] > 1 || pos['y'] > 1);
}
let isInsideDrawingBounds = function(pos) {
if(pos['x'] > xPadding && pos['x'] < (xPadding+drawWidthFactor) && pos['y'] > yPadding && pos['y'] < yPadding+drawHeightFactor) {
return true;
}
return false;
}
let draw = function(e) { let draw = function(e) {
let pos = svgEl.getBoundingClientRect() let pos = getCoordinates(e);
let x = e.x - pos['left'];
let y = e.y - pos['top']; if(!isInsideBounds(pos)) {
// outside of bounds
return;
}
if(isDrawing && !isInsideDrawingBounds(pos)){
stopDrawing(pos);
}
if(!isDrawing && hasMouseDown && isInsideDrawingBounds(pos)){
isDrawing = true;
}
if(isDrawing) { if(isDrawing) {
strokes.push([x, y, 0]); strokes.push([pos['x'], pos['y'], 0]);
let d = strokes2D(strokes); let d = strokes2D(strokes);
strokeEl.setAttribute('d', d); strokeEl.setAttribute('d', d);
} }
currentPoint = { console.log([pos['x'], pos['y']], isDrawing);
socket.send(JSON.stringify({
'action': 'move', 'action': 'move',
'direction': [e.x/window.innerWidth, e.y/window.innerHeight], 'direction': [pos['x'], pos['y']],
'mouse': isDrawing, 'mouse': isDrawing,
}; }));
console.log([x,y], isDrawing);
}; };
let interval = window.setInterval(function(){ let stopDrawing = function(pos) {
if(currentPoint === null) { if(!isDrawing) {
return return;
}
socket.send(JSON.stringify(currentPoint));
}, 200);
let penup = function(e) {
if(!isDrawing) {
return;
} }
isDrawing = false; isDrawing = false;
//document.body.removeEventListener('mousemove', draw); //document.body.removeEventListener('mousemove', draw);
let pos = svgEl.getBoundingClientRect()
let x = e.x - pos['left'];
let y = e.y - pos['top'];
if(strokes.length > 0){ if(strokes.length > 0){
// mark point as last of stroke // mark point as last of stroke
@ -119,9 +229,18 @@
} }
socket.send(JSON.stringify({ socket.send(JSON.stringify({
'action': 'up', 'action': 'up',
'direction': [x, y] 'direction': [pos['x'], pos['y']]
})); }));
}
let penup = function(e) {
if(!hasMouseDown) {
return;
}
hasMouseDown = false;
let pos = getCoordinates(e);
stopDrawing(pos);
}; };
let strokes2D = function(strokes) { let strokes2D = function(strokes) {
// strokes to a d attribute for a path // strokes to a d attribute for a path
@ -130,7 +249,7 @@
let cmd = ""; let cmd = "";
for (let stroke of strokes) { for (let stroke of strokes) {
if(!last_stroke) { if(!last_stroke) {
d += `M${stroke[0]},${stroke[1]} `; d += `M${stroke[0]*svgWidth*10},${stroke[1]*svgHeight*10} `;
cmd = 'M'; cmd = 'M';
} else { } else {
if (last_stroke[2] == 1) { if (last_stroke[2] == 1) {
@ -141,7 +260,7 @@
cmd = 'l'; cmd = 'l';
} }
let rel_stroke = [stroke[0] - last_stroke[0], stroke[1] - last_stroke[1]]; let rel_stroke = [stroke[0] - last_stroke[0], stroke[1] - last_stroke[1]];
d += `${rel_stroke[0]},${rel_stroke[1]} `; d += `${rel_stroke[0]*svgWidth*10},${rel_stroke[1]*svgHeight*10} `;
} }
last_stroke = stroke; last_stroke = stroke;
@ -150,18 +269,16 @@
} }
let startDrawing = function(e){ let startDrawing = function(e){
isDrawing = true; hasMouseDown = true;
// start drawing
//document.body.addEventListener('mousemove', draw);
}; };
let reset = function() {
/*let reset = function() {
strokes = []; strokes = [];
strokeEl.setAttribute('d', ""); strokeEl.setAttribute('d', "");
socket.send(JSON.stringify({ socket.send(JSON.stringify({
'action': 'reset', 'action': 'reset',
})); }));
} }*/
let socket = new WebSocket(url); let socket = new WebSocket(url);
@ -179,8 +296,7 @@
} }
}); });
//svgEl.addEventListener('mousedown', startDrawing); //resetEl.addEventListener('click', reset);
resetEl.addEventListener('click', reset);
submitEl.addEventListener('click', function(e){ submitEl.addEventListener('click', function(e){
if(!strokes.length){ if(!strokes.length){
alert('please draw before submitting'); alert('please draw before submitting');