├── LICENSE ├── README.md ├── pyclick ├── __init__.py ├── _beziercurve.py ├── _utils.py ├── humanclicker.py └── humancurve.py ├── requirements.txt ├── setup.py └── tests ├── __init__.py ├── test_beziercurve.py ├── test_humanclicker.py ├── test_humancurve.py └── test_utils.py /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018 The Python Packaging Authority 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pyclick 2 | This is a library for generating human-like mouse movements. 3 | The movements are based on the concept of bezier curve: 4 | https://en.wikipedia.org/wiki/B%C3%A9zier_curve 5 | 6 | ### Simple Example: 7 | ``` 8 | from pyclick import HumanClicker 9 | 10 | # initialize HumanClicker object 11 | hc = HumanClicker() 12 | 13 | # move the mouse to position (100,100) on the screen in approximately 2 seconds 14 | hc.move((100,100),2) 15 | 16 | # mouse click(left button) 17 | hc.click() 18 | ``` 19 | You can also customize the mouse curve by passing a HumanCurve to HumanClicker. You can control: 20 | - number of internal knots, to change the overall shape of the curve, 21 | - distortion to simulate shivering, 22 | - tween to simulate acceleration and speed of movement 23 | 24 | -------------------------------------------------------------------------------- /pyclick/__init__.py: -------------------------------------------------------------------------------- 1 | name = 'pyclick' 2 | from pyclick.humanclicker import HumanClicker 3 | from pyclick.humancurve import HumanCurve 4 | -------------------------------------------------------------------------------- /pyclick/_beziercurve.py: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | class BezierCurve(): 4 | @staticmethod 5 | def binomial(n, k): 6 | """Returns the binomial coefficient "n choose k" """ 7 | return math.factorial(n) / float(math.factorial(k) * math.factorial(n - k)) 8 | 9 | @staticmethod 10 | def bernsteinPolynomialPoint(x, i, n): 11 | """Calculate the i-th component of a bernstein polynomial of degree n""" 12 | return BezierCurve.binomial(n, i) * (x ** i) * ((1 - x) ** (n - i)) 13 | 14 | @staticmethod 15 | def bernsteinPolynomial(points): 16 | """ 17 | Given list of control points, returns a function, which given a point [0,1] returns 18 | a point in the bezier curve described by these points 19 | """ 20 | def bern(t): 21 | n = len(points) - 1 22 | x = y = 0 23 | for i, point in enumerate(points): 24 | bern = BezierCurve.bernsteinPolynomialPoint(t, i, n) 25 | x += point[0] * bern 26 | y += point[1] * bern 27 | return x, y 28 | return bern 29 | 30 | @staticmethod 31 | def curvePoints(n, points): 32 | """ 33 | Given list of control points, returns n points in the bezier curve, 34 | described by these points 35 | """ 36 | curvePoints = [] 37 | bernstein_polynomial = BezierCurve.bernsteinPolynomial(points) 38 | for i in range(n): 39 | t = i / (n - 1) 40 | curvePoints += bernstein_polynomial(t), 41 | return curvePoints 42 | -------------------------------------------------------------------------------- /pyclick/_utils.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | def isNumeric(val): 4 | return isinstance(val, (float, int, np.int32, np.int64, np.float32, np.float64)) 5 | 6 | def isListOfPoints(l): 7 | if not isinstance(l, list): 8 | return False 9 | try: 10 | isPoint = lambda p: ((len(p) == 2) and isNumeric(p[0]) and isNumeric(p[1])) 11 | return all(map(isPoint, l)) 12 | except (KeyError, TypeError) as e: 13 | return False -------------------------------------------------------------------------------- /pyclick/humanclicker.py: -------------------------------------------------------------------------------- 1 | import pyautogui 2 | from pyclick.humancurve import HumanCurve 3 | 4 | def setup_pyautogui(): 5 | # Any duration less than this is rounded to 0.0 to instantly move the mouse. 6 | pyautogui.MINIMUM_DURATION = 0 # Default: 0.1 7 | # Minimal number of seconds to sleep between mouse moves. 8 | pyautogui.MINIMUM_SLEEP = 0 # Default: 0.05 9 | # The number of seconds to pause after EVERY public function call. 10 | pyautogui.PAUSE = 0.015 # Default: 0.1 11 | 12 | setup_pyautogui() 13 | 14 | class HumanClicker(): 15 | def __init__(self): 16 | pass 17 | 18 | def move(self, toPoint, duration=2, humanCurve=None): 19 | fromPoint = pyautogui.position() 20 | if not humanCurve: 21 | humanCurve = HumanCurve(fromPoint, toPoint) 22 | 23 | pyautogui.PAUSE = duration / len(humanCurve.points) 24 | for point in humanCurve.points: 25 | pyautogui.moveTo(point) 26 | 27 | def click(self): 28 | pyautogui.click() 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /pyclick/humancurve.py: -------------------------------------------------------------------------------- 1 | import pytweening 2 | import numpy as np 3 | import random 4 | from pyclick._utils import isListOfPoints, isNumeric 5 | from pyclick._beziercurve import BezierCurve 6 | 7 | class HumanCurve(): 8 | """ 9 | Generates a human-like mouse curve starting at given source point, 10 | and finishing in a given destination point 11 | """ 12 | 13 | def __init__(self, fromPoint, toPoint, **kwargs): 14 | self.fromPoint = fromPoint 15 | self.toPoint = toPoint 16 | self.points = self.generateCurve(**kwargs) 17 | 18 | def generateCurve(self, **kwargs): 19 | """ 20 | Generates a curve according to the parameters specified below. 21 | You can override any of the below parameters. If no parameter is 22 | passed, the default value is used. 23 | """ 24 | offsetBoundaryX = kwargs.get("offsetBoundaryX", 100) 25 | offsetBoundaryY = kwargs.get("offsetBoundaryY", 100) 26 | leftBoundary = kwargs.get("leftBoundary", min(self.fromPoint[0], self.toPoint[0])) - offsetBoundaryX 27 | rightBoundary = kwargs.get("rightBoundary", max(self.fromPoint[0], self.toPoint[0])) + offsetBoundaryX 28 | downBoundary = kwargs.get("downBoundary", min(self.fromPoint[1], self.toPoint[1])) - offsetBoundaryY 29 | upBoundary = kwargs.get("upBoundary", max(self.fromPoint[1], self.toPoint[1])) + offsetBoundaryY 30 | knotsCount = kwargs.get("knotsCount", 2) 31 | distortionMean = kwargs.get("distortionMean", 1) 32 | distortionStdev = kwargs.get("distortionStdev", 1) 33 | distortionFrequency = kwargs.get("distortionFrequency", 0.5) 34 | tween = kwargs.get("tweening", pytweening.easeOutQuad) 35 | targetPoints = kwargs.get("targetPoints", 100) 36 | 37 | internalKnots = self.generateInternalKnots(leftBoundary,rightBoundary, \ 38 | downBoundary, upBoundary, knotsCount) 39 | points = self.generatePoints(internalKnots) 40 | points = self.distortPoints(points, distortionMean, distortionStdev, distortionFrequency) 41 | points = self.tweenPoints(points, tween, targetPoints) 42 | return points 43 | 44 | def generateInternalKnots(self, \ 45 | leftBoundary, rightBoundary, \ 46 | downBoundary, upBoundary,\ 47 | knotsCount): 48 | """ 49 | Generates the internal knots used during generation of bezier curvePoints 50 | or any interpolation function. The points are taken at random from 51 | a surface delimited by given boundaries. 52 | Exactly knotsCount internal knots are randomly generated. 53 | """ 54 | if not (isNumeric(leftBoundary) and isNumeric(rightBoundary) and 55 | isNumeric(downBoundary) and isNumeric(upBoundary)): 56 | raise ValueError("Boundaries must be numeric") 57 | if not isinstance(knotsCount, int) or knotsCount < 0: 58 | raise ValueError("knotsCount must be non-negative integer") 59 | if leftBoundary > rightBoundary: 60 | raise ValueError("leftBoundary must be less than or equal to rightBoundary") 61 | if downBoundary > upBoundary: 62 | raise ValueError("downBoundary must be less than or equal to upBoundary") 63 | 64 | knotsX = np.random.choice(range(leftBoundary, rightBoundary), size=knotsCount) 65 | knotsY = np.random.choice(range(downBoundary, upBoundary), size=knotsCount) 66 | knots = list(zip(knotsX, knotsY)) 67 | return knots 68 | 69 | def generatePoints(self, knots): 70 | """ 71 | Generates bezier curve points on a curve, according to the internal 72 | knots passed as parameter. 73 | """ 74 | if not isListOfPoints(knots): 75 | raise ValueError("knots must be valid list of points") 76 | 77 | midPtsCnt = max( \ 78 | abs(self.fromPoint[0] - self.toPoint[0]), \ 79 | abs(self.fromPoint[1] - self.toPoint[1]), \ 80 | 2) 81 | knots = [self.fromPoint] + knots + [self.toPoint] 82 | return BezierCurve.curvePoints(midPtsCnt, knots) 83 | 84 | def distortPoints(self, points, distortionMean, distortionStdev, distortionFrequency): 85 | """ 86 | Distorts the curve described by (x,y) points, so that the curve is 87 | not ideally smooth. 88 | Distortion happens by randomly, according to normal distribution, 89 | adding an offset to some of the points. 90 | """ 91 | if not(isNumeric(distortionMean) and isNumeric(distortionStdev) and \ 92 | isNumeric(distortionFrequency)): 93 | raise ValueError("Distortions must be numeric") 94 | if not isListOfPoints(points): 95 | raise ValueError("points must be valid list of points") 96 | if not (0 <= distortionFrequency <= 1): 97 | raise ValueError("distortionFrequency must be in range [0,1]") 98 | 99 | distorted = [] 100 | for i in range(1, len(points)-1): 101 | x,y = points[i] 102 | delta = np.random.normal(distortionMean, distortionStdev) if \ 103 | random.random() < distortionFrequency else 0 104 | distorted += (x,y+delta), 105 | distorted = [points[0]] + distorted + [points[-1]] 106 | return distorted 107 | 108 | def tweenPoints(self, points, tween, targetPoints): 109 | """ 110 | Chooses a number of points(targetPoints) from the list(points) 111 | according to tweening function(tween). 112 | This function in fact controls the velocity of mouse movement 113 | """ 114 | if not isListOfPoints(points): 115 | raise ValueError("points must be valid list of points") 116 | if not isinstance(targetPoints, int) or targetPoints < 2: 117 | raise ValueError("targetPoints must be an integer greater or equal to 2") 118 | 119 | # tween is a function that takes a float 0..1 and returns a float 0..1 120 | res = [] 121 | for i in range(targetPoints): 122 | index = int(tween(float(i)/(targetPoints-1)) * (len(points)-1)) 123 | res += points[index], 124 | return res 125 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | numpy==1.14.2 2 | Pillow==5.0.0 3 | pkg-resources==0.0.0 4 | PyAutoGUI==0.9.36 5 | PyMsgBox==1.0.6 6 | PyScreeze==0.1.14 7 | python3-xlib==0.15 8 | PyTweening==1.0.3 9 | six==1.11.0 10 | xlib==0.21 11 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | with open("README.md", "r") as fh: 4 | long_description = fh.read() 5 | 6 | setuptools.setup( 7 | name='pyclick', 8 | version='0.0.1', 9 | author='patrikoss', 10 | description='Human mouse movement simulation with python', 11 | long_description=long_description, 12 | long_description_content_type='text/markdown', 13 | url="https://github.com/patrikoss/pyclick", 14 | packages=setuptools.find_packages(), 15 | classifiers=[ 16 | "Programming Language :: Python :: 3", 17 | "License :: OSI Approved :: MIT License", 18 | "Operating System :: OS Independent", 19 | ], 20 | install_requires=[ 21 | "numpy>=1.14.2", 22 | "PyAutoGUI>=0.9.36", 23 | "PyTweening>=1.0.3", 24 | "python3-xlib>=0.15", 25 | "xlib>=0.21", 26 | ], 27 | ) 28 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/patrikoss/pyclick/bf0edd19892de54fcae0d78f65b46fd4d6f19f82/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_beziercurve.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from pyclick._beziercurve import BezierCurve 3 | 4 | class TestBezierCurve(unittest.TestCase): 5 | def test_binomial(self): 6 | self.assertEqual(BezierCurve.binomial(10,2), 45) 7 | self.assertEqual(BezierCurve.binomial(4,2), 6) 8 | self.assertEqual(BezierCurve.binomial(1,1), 1) 9 | self.assertEqual(BezierCurve.binomial(2,0), 1) 10 | 11 | def test_bernstein_polynomial_point(self): 12 | self.assertEqual(BezierCurve.bernsteinPolynomialPoint(5,0,0), 1) 13 | 14 | self.assertEqual(BezierCurve.bernsteinPolynomialPoint(3, 0, 1), -2) 15 | self.assertEqual(BezierCurve.bernsteinPolynomialPoint(3, 1, 1), 3) 16 | 17 | self.assertEqual(BezierCurve.bernsteinPolynomialPoint(3, 0, 2), 4) 18 | self.assertEqual(BezierCurve.bernsteinPolynomialPoint(3, 1, 2), -12) 19 | self.assertEqual(BezierCurve.bernsteinPolynomialPoint(3, 2, 2), 9) 20 | 21 | def test_simpleBernsteinPolynomial(self): 22 | bernsteinPolynomial = BezierCurve.bernsteinPolynomial([(0,0), (50,50), (100,100)]) 23 | 24 | self.assertEqual(bernsteinPolynomial(0), (0,0)) 25 | self.assertEqual(bernsteinPolynomial(0.25), (25, 25)) 26 | self.assertEqual(bernsteinPolynomial(0.5), (50, 50)) 27 | self.assertEqual(bernsteinPolynomial(0.75), (75, 75)) 28 | self.assertEqual(bernsteinPolynomial(1), (100,100)) 29 | 30 | 31 | def test_complexBernsteinPolynomial(self): 32 | bernsteinPolynomial = BezierCurve.bernsteinPolynomial([(0,0), (40,40), (100,100)]) 33 | 34 | self.assertEqual(bernsteinPolynomial(0), (0,0)) 35 | self.assertEqual(bernsteinPolynomial(0.25), (21.25,21.25)) 36 | self.assertEqual(bernsteinPolynomial(0.5), (45,45)) 37 | self.assertEqual(bernsteinPolynomial(0.75), (71.25, 71.25)) 38 | self.assertEqual(bernsteinPolynomial(1), (100,100)) 39 | 40 | def test_simpleCurvePoints(self): 41 | points = [(0,0), (50,50), (100,100)] 42 | n = 5 43 | expected_curve_points = [(0,0),(25,25),(50,50),(75,75),(100,100)] 44 | self.assertEqual(BezierCurve.curvePoints(n, points), expected_curve_points) 45 | 46 | 47 | def test_complexCurvePoints(self): 48 | points = [(0,0), (40,40), (100,100)] 49 | n = 5 50 | expected_curve_points = [(0,0),(21.25,21.25),(45,45),(71.25,71.25),(100,100)] 51 | self.assertEqual(BezierCurve.curvePoints(n, points), expected_curve_points) -------------------------------------------------------------------------------- /tests/test_humanclicker.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from pyclick.humanclicker import HumanClicker 3 | import pyautogui 4 | import random 5 | 6 | class TestHumanClicker(unittest.TestCase): 7 | 8 | def test_simple(self): 9 | width, height = pyautogui.size() 10 | toPoint = (width//2, height//2) 11 | hc = HumanClicker() 12 | hc.move(toPoint) 13 | self.assertTrue(pyautogui.position() == toPoint) 14 | 15 | def test_identityMove(self): 16 | toPoint = pyautogui.position() 17 | hc = HumanClicker() 18 | hc.move(toPoint) 19 | self.assertTrue(pyautogui.position() == toPoint) 20 | 21 | def test_randomMove(self): 22 | width, height = pyautogui.size() 23 | toPoint = random.randint(width//2,width-1), random.randint(height//2,height-1) 24 | hc = HumanClicker() 25 | hc.move(toPoint) 26 | self.assertTrue(pyautogui.position() == toPoint) 27 | 28 | -------------------------------------------------------------------------------- /tests/test_humancurve.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import pytweening 3 | from pyclick.humanclicker import HumanCurve 4 | 5 | class TestHumanCurve(unittest.TestCase): 6 | 7 | def test_generateCurve(self): 8 | fromPoint = (100,100) 9 | toPoint = (1000,1000) 10 | hc = HumanCurve(fromPoint, toPoint) 11 | points = hc.generateCurve(offsetBoundaryX=10, offsetBoundaryY=50,\ 12 | leftBoundary=10, rightBoundary=1000, \ 13 | downBoundary=10, upBoundary=1000, \ 14 | knotsCount=5, \ 15 | distortionMean=10, distortionStdev=5, distortionFrequency=0.5, \ 16 | tween=pytweening.easeOutCubic, \ 17 | targetPoints=100) 18 | 19 | self.assertTrue(len(points) == 100) 20 | self.assertTrue(points[0] == fromPoint) 21 | self.assertTrue(points[-1] == toPoint) 22 | 23 | def test_generateInternalKnots(self): 24 | fromPoint = (100,100) 25 | toPoint = (1000,1000) 26 | hc = HumanCurve(fromPoint, toPoint) 27 | 28 | lb, rb, db, ub = 10, 20, 30, 40 29 | knotsCount = 5 30 | internalKnots = hc.generateInternalKnots(lb,rb,db,ub, knotsCount) 31 | self.assertTrue(len(internalKnots) == knotsCount) 32 | for knot in internalKnots: 33 | self.assertTrue(lb <= knot[0] <= rb) 34 | self.assertTrue(db <= knot[1] <= ub) 35 | 36 | def test_generatePoints(self): 37 | fromPoint = (99,99) 38 | toPoint = (100,100) 39 | hc = HumanCurve(fromPoint, toPoint) 40 | 41 | lb, rb, db, ub = 10, 20, 30, 40 42 | knotsCount = 5 43 | internalKnots = hc.generateInternalKnots(lb,rb,db,ub, knotsCount) 44 | points = hc.generatePoints(internalKnots) 45 | 46 | self.assertTrue(len(points) >= 2) 47 | self.assertTrue(points[0] == fromPoint) 48 | self.assertTrue(points[-1] == toPoint) 49 | 50 | def test_distortPoints(self): 51 | points = [(1,1), (2,1), (3,1), (4,1), (5.5,1)] 52 | copyPoints = [pt for pt in points] 53 | fromPoint, toPoint = (1,1), (2,1) 54 | hc = HumanCurve(fromPoint, toPoint) 55 | distorted = hc.distortPoints(points, 0,0,0) 56 | self.assertTrue(distorted == copyPoints) 57 | 58 | def test_tweenPoints(self): 59 | points = [(i,1) for i in range(100,111)] 60 | copyPoints = [pt for pt in points] 61 | fromPoint, toPoint = (1,1), (2,1) 62 | hc = HumanCurve(fromPoint, toPoint) 63 | 64 | targetPoints = 11 65 | tweenConstantFun = lambda x : 0.5 66 | tweened = hc.tweenPoints(points, tweenConstantFun, targetPoints) 67 | self.assertTrue(len(tweened) == targetPoints) 68 | self.assertTrue(tweened == [points[5] for _ in range(targetPoints)]) 69 | -------------------------------------------------------------------------------- /tests/test_utils.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import numpy as np 3 | from pyclick._utils import isNumeric, isListOfPoints 4 | 5 | class TestBezierCurve(unittest.TestCase): 6 | def test_isNumeric(self): 7 | self.assertTrue(isNumeric(1)) 8 | self.assertTrue(isNumeric(1.0)) 9 | self.assertTrue(isNumeric(np.int(0))) 10 | self.assertTrue(isNumeric(np.int32(0))) 11 | self.assertTrue(isNumeric(np.int64(0))) 12 | self.assertTrue(isNumeric(np.float64(2))) 13 | 14 | def test_isNotNumeric(self): 15 | self.assertFalse(isNumeric("asd")) 16 | self.assertFalse(isNumeric(None)) 17 | self.assertFalse(isNumeric([])) 18 | self.assertFalse(isNumeric({})) 19 | self.assertFalse(isNumeric(set())) 20 | 21 | def test_isListOfPoints(self): 22 | self.assertTrue(isListOfPoints([])) 23 | self.assertTrue(isListOfPoints([ 24 | (0, 0), (1.2, 2), (np.float32(2), np.int(0)) 25 | ])) 26 | self.assertTrue([ 27 | [1,2.0] 28 | ]) 29 | 30 | def test_isNotListOfPoints(self): 31 | self.assertFalse(isListOfPoints("asd")) 32 | self.assertFalse(isListOfPoints([ 33 | (1, 2), ("asd", 3) 34 | ])) 35 | self.assertFalse(isListOfPoints([ 36 | [None,None] 37 | ])) 38 | self.assertFalse( 39 | isListOfPoints([2,2]) 40 | ) 41 | self.assertFalse(isListOfPoints(None)) --------------------------------------------------------------------------------