首页 文章

MATLAB中的xkcd样式图

提问于
浏览
206

xkcd-style graph

所以才华横溢的人已经想出如何制作in Mathematicain LaTeXin Pythonin R .

如何使用MATLAB生成如上图所示的图?

我尝试了什么

我创造了扭曲的线条,但我无法得到摇摆的轴 . 我想到的唯一解决方案是用摇摆的线覆盖它们,但我希望能够改变实际的轴 . 我也无法使幽默字体工作,使用的代码位是:

annotation('textbox',[left+left/8 top+0.65*top 0.05525 0.065],...
'String',{'EMBARRASSMENT'},...
'FontSize',24,...
'FontName','Humor',...
'FitBoxToText','off',...
'LineStyle','none');

对于摇摆线,我尝试添加一个小的随机噪声和平滑:

smooth(0.05*randn(size(x)),10)

但是当它们交叉时,我无法使白色背景出现在它们周围......

4 回答

  • 106

    第一步...找到你喜欢的系统字体(使用函数listfonts查看可用的内容)或安装一个与xkcd的手写样式相匹配的字体 . 我在this blog post中提到了用户ch00f的"Humor Sans" TrueType字体,并将其用于我后续的示例 .

    在我看来,你通常需要三个不同的修改图形对象来制作这些类型的图形:axes objectline objecttext object . 您可能还希望annotation object使事情变得更容易,但我现在已经做过,因为它可能比上述三个对象更难实现 .

    我创建了创建三个对象的包装函数,覆盖了某些属性设置,使它们更像xkcd . 一个限制是它们生成的新图形在某些情况下不会更新(比如调整轴的大小时文本对象上的边界框),但这可以通过更完整的面向对象的实现来解决,该实现涉及从handle class继承,使用events and listeners等 . 现在,这是我更简单的实现:

    xkcd_axes.m:

    function hAxes = xkcd_axes(xkcdOptions, varargin)
    
      hAxes = axes(varargin{:}, 'NextPlot', 'add', 'Visible', 'off', ...
                   'XLimMode', 'manual', 'YLimMode', 'manual');
    
      axesUnits = get(hAxes, 'Units');
      set(hAxes, 'Units', 'pixels');
      axesPos = get(hAxes, 'Position');
      set(hAxes, 'Units', axesUnits);
      xPoints = round(axesPos(3)/10);
      yPoints = round(axesPos(4)/10);
      limits = [xlim(hAxes) ylim(hAxes)];
      ranges = [abs(limits(2) - limits(1)) abs(limits(4) - limits(3))];
      backColor = get(get(hAxes, 'Parent'), 'Color');
      xColor = get(hAxes, 'XColor');
      yColor = get(hAxes, 'YColor');
      line('Parent', hAxes, 'Color', xColor, 'LineWidth', 3, ...
           'Clipping', 'off', ...
           'XData', linspace(limits(1), limits(2), xPoints), ...
           'YData', limits(3) + rand(1, xPoints).*0.005.*ranges(2));
      line('Parent', hAxes, 'Color', yColor, 'LineWidth', 3, ...
           'Clipping', 'off', ...
           'YData', linspace(limits(3), limits(4), yPoints), ...
           'XData', limits(1) + rand(1, yPoints).*0.005.*ranges(1));
    
      xTicks = get(hAxes, 'XTick');
      if ~isempty(xTicks)
        yOffset = limits(3) - 0.05.*ranges(2);
        tickIndex = true(size(xTicks));
        if ismember('left', xkcdOptions)
          tickIndex(1) = false;
          xkcd_arrow('left', [limits(1) + 0.02.*ranges(1) xTicks(1)], ...
                     yOffset, xColor);
        end
        if ismember('right', xkcdOptions)
          tickIndex(end) = false;
          xkcd_arrow('right', [xTicks(end) limits(2) - 0.02.*ranges(1)], ...
                     yOffset, xColor);
        end
        plot([1; 1]*xTicks(tickIndex), ...
             0.5.*[-yOffset; yOffset]*ones(1, sum(tickIndex)), ...
             'Parent', hAxes, 'Color', xColor, 'LineWidth', 3, ...
             'Clipping', 'off');
        xLabels = cellstr(get(hAxes, 'XTickLabel'));
        for iLabel = 1:numel(xLabels)
          xkcd_text(xTicks(iLabel), yOffset, xLabels{iLabel}, ...
                    'HorizontalAlignment', 'center', ...
                    'VerticalAlignment', 'middle', ...
                    'BackgroundColor', backColor);
        end
      end
    
      yTicks = get(hAxes, 'YTick');
      if ~isempty(yTicks)
        xOffset = limits(1) - 0.05.*ranges(1);
        tickIndex = true(size(yTicks));
        if ismember('down', xkcdOptions)
          tickIndex(1) = false;
          xkcd_arrow('down', xOffset, ...
                     [limits(3) + 0.02.*ranges(2) yTicks(1)], yColor);
        end
        if ismember('up', xkcdOptions)
          tickIndex(end) = false;
          xkcd_arrow('up', xOffset, ...
                     [yTicks(end) limits(4) - 0.02.*ranges(2)], yColor);
        end
        plot(0.5.*[-xOffset; xOffset]*ones(1, sum(tickIndex)), ...
             [1; 1]*yTicks(tickIndex), ...
             'Parent', hAxes, 'Color', yColor, 'LineWidth', 3, ...
             'Clipping', 'off');
        yLabels = cellstr(get(hAxes, 'YTickLabel'));
        for iLabel = 1:numel(yLabels)
          xkcd_text(xOffset, yTicks(iLabel), yLabels{iLabel}, ...
                    'HorizontalAlignment', 'right', ...
                    'VerticalAlignment', 'middle', ...
                    'BackgroundColor', backColor);
        end
      end
    
      function xkcd_arrow(arrowType, xArrow, yArrow, arrowColor)
        if ismember(arrowType, {'left', 'right'})
          xLine = linspace(xArrow(1), xArrow(2), 10);
          yLine = yArrow + rand(1, 10).*0.003.*ranges(2);
          arrowScale = 0.05.*ranges(1);
          if strcmp(arrowType, 'left')
            xArrow = xLine(1) + arrowScale.*[0 0.5 1 1 1 0.5];
            yArrow = yLine(1) + arrowScale.*[0 0.125 0.25 0 -0.25 -0.125];
          else
            xArrow = xLine(end) - arrowScale.*[0 0.5 1 1 1 0.5];
            yArrow = yLine(end) + arrowScale.*[0 -0.125 -0.25 0 0.25 0.125];
          end
        else
          xLine = xArrow + rand(1, 10).*0.003.*ranges(1);
          yLine = linspace(yArrow(1), yArrow(2), 10);
          arrowScale = 0.05.*ranges(2);
          if strcmp(arrowType, 'down')
            xArrow = xLine(1) + arrowScale.*[0 0.125 0.25 0 -0.25 -0.125];
            yArrow = yLine(1) + arrowScale.*[0 0.5 1 1 1 0.5];
          else
            xArrow = xLine(end) + arrowScale.*[0 -0.125 -0.25 0 0.25 0.125];
            yArrow = yLine(end) - arrowScale.*[0 0.5 1 1 1 0.5];
          end
        end
        line('Parent', hAxes, 'Color', arrowColor, 'LineWidth', 3, ...
             'Clipping', 'off', 'XData', xLine, 'YData', yLine);
        patch('Parent', hAxes, 'FaceColor', arrowColor, ...
              'EdgeColor', arrowColor, 'LineWidth', 2, 'Clipping', 'off', ...
              'XData', xArrow + [0 rand(1, 5).*0.002.*ranges(1)], ...
              'YData', yArrow + [0 rand(1, 5).*0.002.*ranges(2)]);
      end
    
    end
    

    xkcd_text.m:

    function hText = xkcd_text(varargin)
    
      hText = text(varargin{:});
      set(hText, 'FontName', 'Humor Sans', 'FontSize', 13, ...
          'FontWeight', 'normal');
    
      backColor = get(hText, 'BackgroundColor');
      edgeColor = get(hText, 'EdgeColor');
      if ~strcmp(backColor, 'none') || ~strcmp(edgeColor, 'none')
        hParent = get(hText, 'Parent');
        extent = get(hText, 'Extent');
        nLines = size(get(hText, 'String'), 1);
        extent = extent + [-0.5 -0.5 1 1].*0.25.*extent(4)./nLines;
        yPoints = 5*nLines;
        xPoints = round(yPoints*extent(3)/extent(4));
        noiseScale = 0.05*extent(4)/nLines;
        set(hText, 'BackgroundColor', 'none', 'EdgeColor', 'none');
        xBox = [linspace(extent(1), extent(1) + extent(3), xPoints) ...
                extent(1) + extent(3) + noiseScale.*rand(1, yPoints) ...
                linspace(extent(1) + extent(3), extent(1), xPoints) ...
                extent(1) + noiseScale.*rand(1, yPoints)];
        yBox = [extent(2) + noiseScale.*rand(1, xPoints) ...
                linspace(extent(2), extent(2) + extent(4), yPoints) ...
                extent(2) + extent(4) + noiseScale.*rand(1, xPoints) ...
                linspace(extent(2) + extent(4), extent(2), yPoints)];
        patch('Parent', hParent, 'FaceColor', backColor, ...
              'EdgeColor', edgeColor, 'LineWidth', 2, 'Clipping', 'off', ...
              'XData', xBox, 'YData', yBox);
        hKids = get(hParent, 'Children');
        set(hParent, 'Children', [hText; hKids(hKids ~= hText)]);
      end
    
    end
    

    xkcd_line.m:

    function hLine = xkcd_line(xData, yData, varargin)
    
      yData = yData + 0.01.*max(range(xData), range(yData)).*rand(size(yData));
      line(xData, yData, varargin{:}, 'Color', 'w', 'LineWidth', 8);
      hLine = line(xData, yData, varargin{:}, 'LineWidth', 3);
    
    end
    

    这是一个示例脚本,使用这些脚本重新创建上面的漫画 . 我通过使用ginput用鼠标在图中标记点来重新创建线条,捕获它们,然后根据我的需要绘制它们:

    xS = [0.0359 0.0709 0.1004 0.1225 0.1501 0.1759 0.2219 0.2477 0.2974 0.3269 0.3582 0.3895 0.4061 0.4337 0.4558 0.4797 0.5074 0.5276 0.5589 0.5810 0.6013 0.6179 0.6271 0.6344 0.6381 0.6418 0.6529 0.6713 0.6842 0.6934 0.7026 0.7118 0.7265 0.7376 0.7560 0.7726 0.7836 0.7965 0.8149 0.8370 0.8573 0.8867 0.9033 0.9346 0.9659 0.9843 0.9936];
    yS = [0.2493 0.2520 0.2548 0.2548 0.2602 0.2629 0.2629 0.2657 0.2793 0.2657 0.2575 0.2575 0.2602 0.2629 0.2657 0.2766 0.2793 0.2875 0.3202 0.3856 0.4619 0.5490 0.6771 0.7670 0.7970 0.8270 0.8433 0.8433 0.8243 0.7180 0.6199 0.5272 0.4510 0.4128 0.3392 0.2711 0.2275 0.1757 0.1485 0.1131 0.1022 0.0858 0.0858 0.1022 0.1267 0.1567 0.1594];
    
    xF = [0.0304 0.0488 0.0727 0.0967 0.1335 0.1630 0.2090 0.2348 0.2698 0.3011 0.3269 0.3545 0.3803 0.4153 0.4466 0.4724 0.4945 0.5110 0.5350 0.5516 0.5608 0.5700 0.5755 0.5810 0.5884 0.6013 0.6179 0.6363 0.6492 0.6584 0.6676 0.6731 0.6842 0.6860 0.6934 0.7007 0.7136 0.7265 0.7394 0.7560 0.7726 0.7818 0.8057 0.8444 0.8794 0.9107 0.9475 0.9751 0.9917];
    yF = [0.0804 0.0940 0.0967 0.1049 0.1185 0.1458 0.1512 0.1540 0.1649 0.1812 0.1812 0.1703 0.1621 0.1594 0.1703 0.1975 0.2411 0.3065 0.3801 0.4782 0.5708 0.6526 0.7452 0.8106 0.8324 0.8488 0.8433 0.8270 0.7888 0.7343 0.6826 0.5981 0.5300 0.4782 0.3910 0.3420 0.2847 0.2248 0.1621 0.0995 0.0668 0.0395 0.0232 0.0177 0.0204 0.0232 0.0259 0.0204 0.0232];
    
    xE = [0.0267 0.0488 0.0856 0.1409 0.1759 0.2164 0.2514 0.3011 0.3269 0.3637 0.3969 0.4245 0.4503 0.4890 0.5313 0.5608 0.5939 0.6344 0.6694 0.6934 0.7192 0.7394 0.7523 0.7689 0.7891 0.8131 0.8481 0.8757 0.9070 0.9346 0.9604 0.9807 0.9936];
    yE = [0.0232 0.0232 0.0232 0.0259 0.0259 0.0259 0.0313 0.0259 0.0259 0.0259 0.0368 0.0395 0.0477 0.0586 0.0777 0.0886 0.1213 0.1730 0.2466 0.2902 0.3638 0.5082 0.6499 0.7916 0.8924 0.9414 0.9550 0.9387 0.9060 0.8760 0.8542 0.8379 0.8188];
    
    hFigure = figure('Position', [300 300 700 450], 'Color', 'w');
    hAxes = xkcd_axes({'left', 'right'}, 'XTick', [0.45 0.60 0.7 0.8], ...
                      'XTickLabel', {'YARD', 'STEPS', 'DOOR', 'INSIDE'}, ...
                      'YTick', []);
    
    hSpeed = xkcd_line(xS, yS, 'Parent', hAxes, 'Color', [0.5 0.5 0.5]);
    hFear = xkcd_line(xF, yF, 'Parent', hAxes, 'Color', [0 0.5 1]);
    hEmb = xkcd_line(xE, yE, 'Parent', hAxes, 'Color', 'r');
    
    hText = xkcd_text(0.27, 0.9, ...
                      {'WALKING BACK TO MY'; 'FRONT DOOR AT NIGHT:'}, ...
                      'Parent', hAxes, 'EdgeColor', 'k', ...
                      'HorizontalAlignment', 'center');
    
    hSpeedNote = xkcd_text(0.36, 0.35, {'FORWARD'; 'SPEED'}, ...
                           'Parent', hAxes, 'Color', 'k', ...
                           'HorizontalAlignment', 'center');
    hSpeedLine = xkcd_line([0.4116 0.4282 0.4355 0.4411], ...
                           [0.3392 0.3256 0.3038 0.2820], ...
                           'Parent', hAxes, 'Color', 'k');
    hFearNote = xkcd_text(0.15, 0.45, {'FEAR'; 'THAT THERE''S'; ...
                                       'SOMETHING'; 'BEIND ME'}, ...
                          'Parent', hAxes, 'Color', 'k', ...
                          'HorizontalAlignment', 'center');
    hFearLine = xkcd_line([0.1906 0.1998 0.2127 0.2127 0.2201 0.2256], ...
                          [0.3501 0.3093 0.2629 0.2221 0.1975 0.1676], ...
                          'Parent', hAxes, 'Color', 'k');
    hEmbNote = xkcd_text(0.88, 0.45, {'EMBARRASSMENT'}, ...
                         'Parent', hAxes, 'Color', 'k', ...
                         'HorizontalAlignment', 'center');
    hEmbLine = xkcd_line([0.8168 0.8094 0.7983 0.7781 0.7578], ...
                         [0.4864 0.5436 0.5872 0.6063 0.6226], ...
                         'Parent', hAxes, 'Color', 'k');
    

    而且(小号)这是由此产生的情节!:

    enter image description here

  • 26

    我没有重新实现所有各种绘图功能,而是想创建一个可以将任何现有绘图转换为xkcd样式绘图的通用工具 .

    这种方法意味着您可以使用标准MATLAB函数创建绘图并对其进行样式设置,然后在完成后,您可以在保留绘图的整体样式的同时以xkcd样式重新渲染绘图 .

    例子

    Plot
    Enter image description here

    Bar & Plot

    Enter image description here

    Box & Plot
    Enter image description here

    它是如何工作的

    该函数通过迭代轴的子项来工作 . 如果孩子的类型为 linepatch ,则会略微扭曲他们 . 如果子类型为 hggroup ,则迭代 hggroup 的子子元素 . 我计划支持其他情节类型,例如 image ,但目前尚不清楚扭曲图像以获得xkcd风格的最佳方法是什么 .

    最后,为了确保失真看起来是均匀的(即,短线不会比长线更加分散),我以像素为单位测量线长度,然后以与其长度成比例的方式测量样本 . 然后,我为每个第N个样本添加噪声,这些样本产生的线条具有或多或少相同的失真量 .

    守则

    而不是粘贴数百行代码,我只会链接到gist of the source . 此外,源代码和生成上述示例的代码可以免费获得GitHub .

    正如您从示例中看到的那样,虽然我计划在找到最佳方法时立即实施,但它还没有扭曲轴本身 .

  • 59

    我看到两种解决方法:第一种方法是在绘图要素的x / y坐标上添加一些抖动 . 这样做的好处是您可以轻松修改绘图,但如果您想让它们xkcdyfied(参见@Rody Oldenhuis' solution),则必须自己绘制轴 . 第二种方法是创建一个非抖动的图,并使用 imtransform 对图像应用随机失真 . 这样做的好处是可以在任何情节中使用它,但最终会得到一个图像,而不是一个可编辑的图 .

    我将首先显示#2,然后尝试#1(如果您更喜欢#1,请查看Rody's solution!) .

    enter image description here

    此解决方案依赖于两个关键功能:来自文件交换的EXPORT_FIG以获得抗锯齿的屏幕截图,以及IMTRANSFORM来获得转换 .

    %# define plot data
    x = 1:0.1:10;
    y1 = sin(x).*exp(-x/3) + 3;
    y2 = 3*exp(-(x-7).^2/2) + 1;
    
    %# plot
    fh = figure('color','w');
    hold on
    plot(x,y1,'b','lineWidth',3);
    plot(x,y2,'w','lineWidth',7);
    plot(x,y2,'r','lineWidth',3);
    
    xlim([0.95 10])
    ylim([0 5])
    set(gca,'fontName','Comic Sans MS','fontSize',18,'lineWidth',3,'box','off')
    
    %# add an annotation 
     annotation(fh,'textarrow',[0.4 0.55],[0.8 0.65],...
         'string',sprintf('text%shere',char(10)),'headStyle','none','lineWidth',1.5,...
         'fontName','Comic Sans MS','fontSize',14,'verticalAlignment','middle','horizontalAlignment','left')
    
    %# capture with export_fig
    im = export_fig('-nocrop',fh);
    
    %# add a bit of border to avoid black edges
    im = padarray(im,[15 15 0],255);
    
    %# make distortion grid
    sfc = size(im);
    [yy,xx]=ndgrid(1:7:sfc(1),1:7:sfc(2));
    pts = [xx(:),yy(:)];
    tf = cp2tform(pts+randn(size(pts)),pts,'lwm',12);
    w = warning;
    warning off images:inv_lwm:cannotEvaluateTransfAtSomeOutputLocations
    imt = imtransform(im,tf);
    warning(w)
    
    %# remove padding
    imt = imt(16:end-15,16:end-15,:);
    
    figure('color','w')
    imshow(imt)
    

    这是我最初的抖动尝试

    enter image description here

    %# define plot data
    x = 1:0.1:10;
    y1 = sin(x).*exp(-x/3) + 3;
    y2 = 3*exp(-(x-7).^2/2) + 1;
    
    %# jitter
    x = x+randn(size(x))*0.01;
    y1 = y1+randn(size(x))*0.01;
    y2 = y2+randn(size(x))*0.01;
    
    %# plot
    figure('color','w')
    hold on
    plot(x,y1,'b','lineWidth',3);
    plot(x,y2,'w','lineWidth',7);
    plot(x,y2,'r','lineWidth',3);
    
    xlim([0.95 10])
    ylim([0 5])
    set(gca,'fontName','Comic Sans MS','fontSize',18,'lineWidth',3,'box','off')
    
  • 89

    那么,这是我的不那么粗暴但仍然不太完美的尝试:

    %# init
    %# ------------------------
    
    noise = @(x,A) A*randn(size(x));
    ns    = @(x,A) A*ones(size(x));
    
    
    h = figure(2); clf, hold on
    pos = get(h, 'position');
    set(h, 'position', [pos(1:2) 800 450]);
    
    
    blackline = {
        'k', ...
        'linewidth', 2};
    axisline = {
        'k', ...
        'linewidth', 3};
    
    textprops = {
        'fontName','Comic Sans MS',...
        'fontSize', 14,...
        'lineWidth',3};
    
    
    %# Plot data
    %# ------------------------
    x  = 1:0.1:10;
    
    y0 = sin(x).*exp(-x/30) + 3;
    y1 = sin(x).*exp(-x/3) + 3;
    y2 = 3*exp(-(x-7).^6/.05) + 1;
    
    y0 = y0 + noise(x, 0.01);
    y1 = y1 + noise(x, 0.01);
    y2 = y2 + noise(x, 0.01);
    
    %# plot
    plot(x,y0, 'color', [0.7 0.7 0.7], 'lineWidth',3);
    
    plot(x,y1, 'w','lineWidth',7);
    plot(x,y1, 'b','lineWidth',3);
    
    plot(x,y2, 'w','lineWidth',7);
    plot(x,y2, 'r','lineWidth',3);
    
    
    
    
    %# text
    %# ------------------------
    ll(1) = text(1.3, 4.2,...
        {'Walking back to my'
        'front door at night:'});
    
    ll(2) = text(5, 0.7, 'yard');
    ll(3) = text(6.2, 0.7, 'steps');
    ll(4) = text(7, 0.7, 'door');
    ll(5) = text(8, 0.7, 'inside');
    
    set(ll, textprops{:});
    
    
    %# arrows & lines
    %# ------------------------
    
    %# box around "walking back..."
    xx = 1.2:0.1:3.74;
    yy = ns(xx, 4.6) + noise(xx, 0.007);
    plot(xx, yy, blackline{:})
    
    xx = 1.2:0.1:3.74;
    yy = ns(xx, 3.8) + noise(xx, 0.007);
    plot(xx, yy, blackline{:})
    
    yy = 3.8:0.1:4.6;
    xx = ns(yy, 1.2) + noise(yy, 0.007);
    plot(xx, yy, blackline{:})
    
    xx = ns(yy, 3.74) + noise(yy, 0.007);
    plot(xx, yy, blackline{:})
    
    %# left arrow
    x_arr = 1.2:0.1:4.8;
    y_arr = 0.65 * ones(size(x_arr)) + noise(x_arr, 0.005);
    plot(x_arr, y_arr, blackline{:})
    x_head = [1.1 1.6 1.62];
    y_head = [0.65 0.72 0.57];
    patch(x_head, y_head, 'k')
    
    %# right arrow
    x_arr = 8.7:0.1:9.8;
    y_arr = 0.65 * ones(size(x_arr)) + noise(x_arr, 0.005);
    plot(x_arr, y_arr, blackline{:})
    x_head = [9.8 9.3 9.3];
    y_head = [0.65 0.72 0.57];
    patch(x_head, y_head, 'k')
    
    %# left line on axis
    y_line = 0.8:0.1:1.1;
    x_line = ns(y_line, 6.5) + noise(y_line, 0.005);
    plot(x_line, y_line, blackline{:})
    
    %# right line on axis
    y_line = 0.8:0.1:1.1;
    x_line = ns(y_line, 7.2) + noise(y_line, 0.005);
    plot(x_line, y_line, blackline{:})
    
    %# axes
    x_xax = x;
    y_xax = 0.95 + noise(x_xax, 0.01);
    y_yax = 0.95:0.1:5;
    x_yax = x(1) + noise(y_yax, 0.01);
    plot(x_xax, y_xax, axisline{:})
    plot(x_yax, y_yax, axisline{:})
    
    
    % finalize 
    %# ------------------------
    
    xlim([0.95 10])
    ylim([0 5])
    axis off
    

    结果:

    XKCD imitation in Matlab

    要做的事:

    • 找到更好的功能(更好地分段定义它们)

    • 将"annotations"和波浪线添加到它们描述的曲线中

    • 找到比Comic Sans更好的字体!

    • 将所有内容概括为函数 plot2xkcd ,以便我们可以将任何绘图/图形转换为xkcd样式 .

相关问题