diff --git a/src/geomtools/__init__.py b/src/geomtools/__init__.py
index cc94950e98db73327f6e9091f1d56a7d32bc4b92..abf1c681995761910323978f20f021b671c581a8 100644
--- a/src/geomtools/__init__.py
+++ b/src/geomtools/__init__.py
@@ -2,5 +2,6 @@
 from .sfx import (
     parse_crystfel_streamfile, read_crystfel_streamfile, extract_geometry,
     plot_center_shift, plot_cell_parameters, plot_peakogram, plot_powder,
+    plot_detector, ph_en_to_lambda, get_q_from_xyz, get_min_bragg_dist,
     spacing, gauss2d_fit, ellipse, parse_xwiz_summary, get_peak_position,
 )
diff --git a/src/geomtools/detector/__init__.py b/src/geomtools/detector/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..3e87147631d3f18d42ac79e13f3d5a215ec640b9
--- /dev/null
+++ b/src/geomtools/detector/__init__.py
@@ -0,0 +1,5 @@
+# flake8: noqa E401
+from .geom import (
+    read_crystfel_geom, get_pixel_positions, assemble_data
+)
+from .draw import plot_detector_layout, plot_data_on_detector
diff --git a/src/geomtools/detector/draw.py b/src/geomtools/detector/draw.py
new file mode 100644
index 0000000000000000000000000000000000000000..dbc78462ea57f4dc66979f6502960931355884be
--- /dev/null
+++ b/src/geomtools/detector/draw.py
@@ -0,0 +1,65 @@
+import numpy as np
+
+import matplotlib.pyplot as plt
+from mpl_toolkits.axes_grid1 import make_axes_locatable
+
+from .geom import assemble_data, get_pixel_positions
+
+
+def plot_detector_layout(panels, figsize=(16, 16), **kwargs):
+    """Draw the detector panels according to the detector geometry."""
+    rect = []
+    xmn, xmx, ymn, ymx = np.inf, -np.inf, np.inf, -np.inf
+    for name, p in panels.iterrows():
+        x0, y0 = p.cnx, p.cny
+        x1 = p.cnx + p.w * p.fsx + p.h * p.ssx
+        y1 = p.cny + p.w * p.fsy + p.h * p.ssy
+        rect.append(np.array([[x0, y0], [x1, y0], [x1, y1], [x0, y1]]))
+        xmn, xmx = min(xmn, x0, x1), max(xmx, x1, x0)
+        ymn, ymx = min(ymn, y0, y1), max(ymx, y1, y0)
+
+    width = xmx - xmn + 4
+    height = ymx - ymn + 4
+    figsize = (16, 16 / width * height)
+    fig, ax = plt.subplots(1, 1, figsize=figsize, **kwargs)
+    ax.set_xlim(xmn - 2, xmx + 2)
+    ax.set_ylim(ymn - 2, ymx + 2)
+    ax.axis(False)
+    ax.set_aspect('equal')
+    for r in rect:
+        ax.add_patch(plt.Polygon(r, fill=False, color='lightgray'))
+
+    return fig, ax
+
+
+def plot_data_on_detector(data, panels, figwidth=16, cmap=plt.cm.magma,
+                          **kwargs):
+    """Plots data according to the detector geometry."""
+
+    if isinstance(panels, np.ndarray):
+        pos = panels
+    else:
+        pos = get_pixel_positions(panels)
+
+    img, (xc, yc) = assemble_data(data, pos)
+    msk, _ = assemble_data(np.ones(data.shape, bool), pos)
+    img = np.ma.masked_array(img, ~msk)
+    ny, nx = img.shape
+
+    figsize = (figwidth, figwidth / nx * ny)
+    fig, ax = plt.subplots(1, 1, figsize=figsize, **kwargs)
+
+    extent = (nx - xc + 0.5, -xc - 0.5, -yc - 0.5, ny - yc + 0.5)
+    im = ax.imshow(np.flip(img, axis=1), origin='lower',
+                   extent=extent, vmin=0, vmax=1, cmap=cmap)
+    ax.plot([-50, 50], [0, 0], 'r')
+    ax.plot([0, 0], [-50, 50], 'r')
+
+    ax.set_aspect('equal')
+    ax.axis(False)
+
+    ax_divider = make_axes_locatable(ax)
+    cax = ax_divider.append_axes("right", size="5%", pad="2%")
+
+    fig.colorbar(im, ax=ax, cax=cax)
+    return fig, ax
diff --git a/src/geomtools/detector/geom.py b/src/geomtools/detector/geom.py
new file mode 100644
index 0000000000000000000000000000000000000000..11e6129b37648c5f82f24f1ccd59adbcc2e2fda8
--- /dev/null
+++ b/src/geomtools/detector/geom.py
@@ -0,0 +1,97 @@
+import numpy as np
+import pandas as pd
+
+from cfelpyutils.geometry import load_crystfel_geometry
+
+
+def append_rigid_group(panels, geom, group_name):
+    """Appends the rigid group name to the panels."""
+    groups = dict(
+        (rg, geom.detector['rigid_groups'][rg])
+        for rg in geom.detector['rigid_group_collections'][group_name]
+    )
+
+    ix, pa = [], []
+    for m, p in groups.items():
+        ix += [m] * len(p)
+        pa += p
+
+    panels = panels.join(
+        pd.DataFrame(data={group_name: ix, 'panel': pa}).set_index('panel'))
+
+    return panels
+
+
+def read_crystfel_geom(filename, indexes=dict()):
+    """Reads Crystfel geometry file."""
+    geom = load_crystfel_geometry(filename)
+
+    panels = pd.DataFrame(
+        geom.detector['panels'].values(),
+        index=geom.detector['panels'].keys()
+    )
+    panels.index.name = 'panel'
+
+    for group_name in geom.detector['rigid_group_collections']:
+        panels = append_rigid_group(panels, geom, group_name)
+
+    for column, dimno in indexes.items():
+        panels[column] = panels.dim_structure.apply(lambda x: x[dimno])
+
+    return panels
+
+
+def get_pixel_positions(panels):
+    """Returns pixel positions according to the detector geometry."""
+    lo = [1 << 63, 1 << 63, 1 << 63]
+    hi = [-1 << 63, -1 << 63, -1 << 63]
+    for pn, pv in panels.iterrows():
+        sh = pv.dim_structure[:]
+        sh.remove('%')
+
+        repl = {'fs': pv.orig_min_fs, 'ss': pv.orig_min_ss}
+        lo = min(lo, list(map(lambda x: repl.get(x, x), sh)))
+
+        repl = {'fs': pv.orig_max_fs, 'ss': pv.orig_max_ss}
+        hi = max(hi, list(map(lambda x: repl.get(x, x) + 1, sh)))
+
+    shape = [b - a for a, b in zip(lo, hi)]
+    crd = np.zeros(shape + [3], float)
+
+    for pn, pv, in panels.iterrows():
+        sh = pv.dim_structure[:]
+        sh.remove('%')
+
+        ss, fs = np.meshgrid(
+            range(pv.orig_min_ss, pv.orig_max_ss + 1),
+            range(pv.orig_min_fs, pv.orig_max_fs + 1),
+            indexing="ij"
+        )
+
+        repl = {'fs': fs, 'ss': ss}
+        ix = tuple(map(lambda x: repl.get(x[0], x[0]) - x[1], zip(sh, lo)))
+
+        ss -= pv.orig_min_ss
+        fs -= pv.orig_min_fs
+
+        crd[ix + (0,)] = ss * pv.ssx + fs * pv.fsx + pv.cnx
+        crd[ix + (1,)] = ss * pv.ssy + fs * pv.fsy + pv.cny
+        crd[ix + (2,)] = ss * pv.ssz + fs * pv.fsz
+
+    return crd
+
+
+def assemble_data(data, pos):
+    """Assembles data in image according to the detector geometry."""
+    x = (pos[..., 0] + 0.5).astype(int)
+    y = (pos[..., 1] + 0.5).astype(int)
+
+    xmin, xmax = np.min(x), np.max(x)
+    ymin, ymax = np.min(y), np.max(y)
+
+    nx, ny = xmax - xmin + 1, ymax - ymin + 1
+
+    img = np.zeros([ny, nx], dtype=data.dtype)
+    img[y - ymin, x - xmin] = data
+
+    return img, (-xmin, -ymin)
diff --git a/src/geomtools/sfx/__init__.py b/src/geomtools/sfx/__init__.py
index 76fa95bff471e297ede008cdfd0160367c240133..966ba9b884858046ef5291472c2c51be4c448ae5 100644
--- a/src/geomtools/sfx/__init__.py
+++ b/src/geomtools/sfx/__init__.py
@@ -4,7 +4,10 @@ from .crystfelio import (
 )
 from .draw import (
     plot_center_shift, plot_cell_parameters, plot_peakogram, plot_powder,
+    plot_detector,
+)
+from .lattice import (
+    spacing, ph_en_to_lambda, get_q_from_xyz, get_min_bragg_dist,
 )
-from .lattice import spacing
 from .misc import gauss2d_fit, ellipse, get_peak_position
 from .xwizio import parse_xwiz_summary
diff --git a/src/geomtools/sfx/draw.py b/src/geomtools/sfx/draw.py
index c9bd27794293929f2a1e2a9caf0d3a3289abb977..07eadd44c1b29831ab231fc450f29c4f25426004 100644
--- a/src/geomtools/sfx/draw.py
+++ b/src/geomtools/sfx/draw.py
@@ -126,3 +126,28 @@ def plot_powder(peaks, counts=True, figwidth=14, frameon=False, **kwargs):
     ax.axis(False)
 
     return fig, ax
+
+
+def plot_detector(panels, figsize=(16, 16), **kwargs):
+    rect = []
+    xmn, xmx, ymn, ymx = np.inf, -np.inf, np.inf, -np.inf
+    for name, p in panels.iterrows():
+        x0, y0 = p.cnx, p.cny
+        x1 = p.cnx + p.w * p.fsx + p.h * p.ssx
+        y1 = p.cny + p.w * p.fsy + p.h * p.ssy
+        rect.append(np.array([[x0, y0], [x1, y0], [x1, y1], [x0, y1]]))
+        xmn, xmx = min(xmn, x0, x1), max(xmx, x1, x0)
+        ymn, ymx = min(ymn, y0, y1), max(ymx, y1, y0)
+
+    width = xmx - xmn + 4
+    height = ymx - ymn + 4
+    figsize = (16, 16 / width * height)
+    fig, ax = plt.subplots(1, 1, figsize=figsize, **kwargs)
+    ax.set_xlim(xmn - 2, xmx + 2)
+    ax.set_ylim(ymn - 2, ymx + 2)
+    ax.axis(False)
+    ax.set_aspect('equal')
+    for r in rect:
+        ax.add_patch(plt.Polygon(r, fill=False, color='lightgray'))
+
+    return fig, ax