diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000000000000000000000000000000000000..07c497e74357af082d0142c43a2d37e2c039c8ea
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,40 @@
+# Changelog
+
+All changes that impact users of this project will be documented in this file.
+
+The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
+and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+
+<!---
+This document is intended for users of the applications and API. Changes to things
+like tests should not be noted in this document.
+
+When updating this file for a PR, add an entry for your change under Unreleased
+and one of the following headings:
+ - Added - for new features.
+ - Changed - for changes in existing functionality.
+ - Deprecated - for soon-to-be removed features.
+ - Removed - for now removed features.
+ - Fixed - for any bug fixes.
+ - Security - in case of vulnerabilities.
+
+If the heading does not yet exist under Unreleased, then add it as a 3rd heading,
+with three #.
+
+
+When preparing for a public release candidate add a new 2nd heading, with two #, under
+Unreleased with the version number and the release date, in year-month-day
+format. Then, add a link for the new version at the bottom of this document and
+update the Unreleased link so that it compares against the latest release tag.
+
+
+When preparing for a bug fix release create a new 2nd heading above the Fixed
+heading to indicate that only the bug fixes and security fixes are in the bug fix
+release.
+-->
+
+## Unreleased
+
+### Changed
+- Removed all `pyproj` calls from csm.py, abstracting them into the reprojection and pyproj.Transformer code inside utils.py. Updated the transformations to use the new pipeline style syntax to avoid deprecation warnings about old syntax.p
+
diff --git a/knoten/csm.py b/knoten/csm.py
index 0c627905ab691b8c170ce5c3e2452d613a8aac09..ee703142c56d4cb9eb3260429c0c932f7a8b83e6 100644
--- a/knoten/csm.py
+++ b/knoten/csm.py
@@ -6,13 +6,13 @@ from csmapi import csmapi
 import jinja2
 from osgeo import ogr
 import numpy as np
-import pyproj
 import requests
 import scipy.stats
 from functools import singledispatch
 
 from plio.io.io_gdal import GeoDataset
 
+from knoten import utils
 class NumpyEncoder(json.JSONEncoder):
     def default(self, obj):
         if isinstance(obj, np.ndarray):
@@ -157,10 +157,16 @@ def _(dem, image_pt, camera, max_its = 20, tolerance = 0.001):
     intersection = generate_ground_point(0.0, image_pt, camera)
     iterations = 0
     semi_major, semi_minor = get_radii(camera)
-    ecef = pyproj.Proj(proj='geocent', a=semi_major, b=semi_minor)
-    lla = pyproj.Proj(proj='latlon', a=semi_major, b=semi_minor)
+    
+    source_proj = f'+proj=cart +a={semi_major} +b={semi_minor}'
+    dest_proj = f'+proj=lonlat +a={semi_major} +b={semi_minor}'
+    transformer = utils.create_transformer(source_proj, dest_proj)
+
     while iterations != max_its:
-        lon, lat, alt = pyproj.transform(ecef, lla, intersection.x, intersection.y, intersection.z)
+        lon, lat, alt = transformer.transform(intersection.x, 
+                                              intersection.y, 
+                                              intersection.z,
+                                              errcheck=True)
 
         px, py = dem.latlon_to_pixel(lat, lon)
         height = dem.read_array(1, [px, py, 1, 1])[0][0]
@@ -231,8 +237,9 @@ def generate_latlon_boundary(camera, boundary, dem=0.0, radii=None, **kwargs):
     else:
         semi_major, semi_minor = radii
 
-    ecef = pyproj.Proj(proj='geocent', a=semi_major, b=semi_minor)
-    lla = pyproj.Proj(proj='latlon', a=semi_major, b=semi_minor)
+    source_proj = f'+proj=cart +a={semi_major} +b={semi_minor}'
+    dest_proj = f'+proj=lonlat +a={semi_major} +b={semi_minor}'
+    transformer = utils.create_transformer(source_proj, dest_proj)
 
     gnds = np.empty((len(boundary), 3))
 
@@ -245,7 +252,7 @@ def generate_latlon_boundary(camera, boundary, dem=0.0, radii=None, **kwargs):
 
         gnds[i] = [gnd.x, gnd.y, gnd.z]
 
-    lons, lats, alts = pyproj.transform(ecef, lla, gnds[:,0], gnds[:,1], gnds[:,2])
+    lons, lats, alts = transformer.transform(gnds[:,0], gnds[:,1], gnds[:,2])
     return lons, lats, alts
 
 def generate_gcps(camera, boundary, radii=None):
@@ -392,16 +399,16 @@ def generate_bodyfixed_footprint(camera, boundary, radii=None):
 
     latlon_fp = generate_latlon_footprint(camera, boundary, radii=radii)
 
-    ecef = pyproj.Proj(proj='geocent', a=semi_major, b=semi_minor)
-    lla = pyproj.Proj(proj='latlon', a=semi_major, b=semi_minor)
-
+    source_proj = f'+proj=lonlat +a={semi_major} +b={semi_minor}'
+    dest_proj = f'+proj=cart +a={semi_major} +b={semi_minor}'
+    transformer = utils.create_transformer(source_proj, dest_proj)
     # Step over all geometry objects in the latlon footprint
     for i in range(latlon_fp.GetGeometryCount()):
         latlon_coords = np.array(latlon_fp.GetGeometryRef(i).GetGeometryRef(0).GetPoints())
 
         # Check if the geometry object is populated with points
         if len(latlon_coords) > 0:
-            x, y, z = pyproj.transform(lla, ecef,  latlon_coords[:,0], latlon_coords[:,1], latlon_coords[:,2])
+            x, y, z = transformer.transform(latlon_coords[:,0], latlon_coords[:,1], latlon_coords[:,2])
 
             # Step over all coordinate points in a geometry object and update said point
             for j, _ in enumerate(latlon_coords):
diff --git a/knoten/utils.py b/knoten/utils.py
index 47bc704747219209f19bb38144f5e670b5547329..121721ad7890b0fc334ad1aac4faa044b91f8789 100644
--- a/knoten/utils.py
+++ b/knoten/utils.py
@@ -35,9 +35,17 @@ def reproject(record, semi_major, semi_minor, source_proj, dest_proj, **kwargs):
     Transformed coordinates as y, x, z
 
     """
-    source_pyproj = pyproj.Proj(proj = source_proj, a = semi_major, b = semi_minor)
-    dest_pyproj = pyproj.Proj(proj = dest_proj, a = semi_major, b = semi_minor)
-
-    y, x, z = pyproj.transform(source_pyproj, dest_pyproj, record[0], record[1], record[2], **kwargs)
+    transformer = pyproj.Transformer.from_crs(f'+proj={source_proj} +a={semi_major} +b={semi_minor}',
+                                              f'+proj={dest_proj} +a={semi_major} +b={semi_minor}',
+                                              always_xy=True)
+    source_proj = f'+proj={source_proj} +a={semi_major} +b={semi_minor}'
+    dest_proj = f'+proj={dest_proj} +a={semi_major} +b={semi_minor}'
+    transformer = create_transformer(source_proj, dest_proj)
+    x, y, z = transformer.transform(record[0], record[1], record[2], errcheck=True)
 
     return y, x, z
+
+def create_transformer(source_proj, dest_proj):
+    return pyproj.Transformer.from_crs(source_proj,
+                                       dest_proj,
+                                       always_xy=True)
\ No newline at end of file
diff --git a/tests/test_reproject.py b/tests/test_reproject.py
index 0b479af862e41825ac822fbdaf1e307f8c40c47f..52fe2068f11a2c16d45463cb417648ac2695601d 100644
--- a/tests/test_reproject.py
+++ b/tests/test_reproject.py
@@ -4,7 +4,8 @@ import pytest
 from knoten import utils
 
 def test_reproject():
-    with mock.patch('pyproj.transform', return_value=[1,1,1]) as mock_pyproj:
-        res = utils.reproject([1,1,1], 10, 10, 'geocent', 'latlon')
-        mock_pyproj.assert_called_once()
-        assert res == (1,1,1)
+    res = utils.reproject([0,1,0], 10, 10, 'geocent', 'latlon')
+    print(res)
+    assert res[0] == 0
+    assert res[1] == 90
+    assert res[2] == -9