상세 컨텐츠

본문 제목

JAVA 로 워드클라우드 생성하기

AI/빅데이터

by cepiloth 2021. 3. 31. 12:20

본문

728x90
반응형

https://github.com/epicdevs/Word-Cloud/

분석해 볼 만한 소스를 찾았다.

 

StringProcessor class 분석

문장을 읽으며 " " 기준으로 문자열을 읽고
removePunctuations 메서드를 통해 {} 대괄호, 숫자 등을 제거한 순수 단어만 추출한다
filter.contains를 통해 blacklist(제외) 등록된 단어인 경우 제외한다
HashMap에 단어를 등록하면서 weight를 적산한다
이후 우선순위 큐에 weight 기준으로 원소를 삽입한다.

public class StringProcessor implements Iterable<WordCount>{
    private String str;
    private final HashSet<String> filter;
    private ArrayList<WordCount> words;
    
    public StringProcessor(String str, HashSet<String> filter) {
        this.str = str;
        this.filter = filter;
        processString();
    }
    
    private void processString() {
        Scanner scan = new Scanner(str);
        HashMap<String, Integer> count = new HashMap<String, Integer>();
        while (scan.hasNext()) {
            String word = removePunctuations(scan.next());

            // filter 단어를 제외
            if (filter.contains(word)) continue;

            // 공백 문자 제외
            if (word.equals("")) continue;

            Integer n = count.get(word);
            count.put(word, (n == null) ? 1 : n + 1);
        }
        PriorityQueue<WordCount> pq = new PriorityQueue<WordCount>();
        for (Entry<String, Integer> entry : count.entrySet()) {
            pq.add(new WordCount(entry.getKey(), entry.getValue()));
        }
        words = new ArrayList<WordCount>();
        while (!pq.isEmpty()) {
            WordCount wc = pq.poll();
            if (wc.word.length() > 1) words.add(wc);
        }
    }
    
    public void print() {
        Iterator<WordCount> iter = iterator();
        while (iter.hasNext()) {
            System.out.println(iter.next());
        }
    }

    @Override
    public Iterator<WordCount> iterator() {
        return words.iterator();
    }
    
    private static String removePunctuations(String str) {
        return str.replaceAll("\\p{Punct}|\\p{Digit}", "");
    }
}

 

CloudImageGenerator class 분석

 비트맵을 생성하고 StringProcessor class에서 수집한 weight 가 있는 단어로 Font 설정 및 그리는 영역을 구한다. 그려지는 외곽의 영역을 private ArrayList <Shape> occupied 배열에 원소로 삽입하고, 다음 글자를 그릴 때 Intersects(교차영역)이 있으면 다시 shape을 생성하고 그려질 영역의 outline의 폴리곤 정보를 추출한다.

 모든 단어를 반복해서 수행함
for (int i = 0; i < REJECT_COUNT * wc.n * 2; i++)  해당 for 문의 bruth-force로 구성되어 있어 속도에 문제가 있어 보인다.

public class CloudImageGenerator {
    private static final int REJECT_COUNT = 100;
    private static final int LARGEST_FONT_SIZE = 160;
    private static final int FONT_STEP_SIZE = 5;
    private static final int MINIMUM_FONT_SIZE = 20;
    private static final int MINIMUM_WORD_COUNT = 2;
//    public static final String FONT_FAMILY = "나눔명조";
    public static final String FONT_FAMILY = "Helvetica";
    public static final String[] THEME = ColorCombinations.THEME1;
    
    private String fontFamily;
    private final int width;
    private final int height;
    private final int padding;
    private BufferedImage bi;
    private ColorCombinations colorTheme;
    private ArrayList<Shape> occupied = new ArrayList<Shape>();
    
    public CloudImageGenerator(int width, int height, int padding) {
        this.width = width;
        this.height = height;
        this.fontFamily = FONT_FAMILY;
        this.padding = padding;
    }

    /**
     * This algorithm can be fancier than this sloppy random generation algorithm
     * For more information: http://static.mrfeinberg.com/bv_ch03.pdf
     */
    public BufferedImage generateImage(Iterable<WordCount> words, long seed) {
        Random rand = new Random(seed);
        bi = new BufferedImage(width + 2 * padding, height + 2 * padding, BufferedImage.TYPE_INT_ARGB);
        colorTheme = new ColorCombinations(THEME);
        Graphics2D g = bi.createGraphics();
        g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        g.setColor(colorTheme.background());
        g.fillRect(0, 0, bi.getWidth(), bi.getHeight());
        g.translate(padding, padding);
        
        Iterator<WordCount> iter = words.iterator();
        int k = LARGEST_FONT_SIZE;
        while (iter.hasNext()) {
            WordCount wc = iter.next();
            if (wc.n < MINIMUM_WORD_COUNT) break;
            int prevK = k;
            if (k > MINIMUM_FONT_SIZE) k = k - FONT_STEP_SIZE;
            Font font = new Font(fontFamily, Font.BOLD, k);
            g.setFont(font);
            FontMetrics fm = g.getFontMetrics();
            Shape s = stringShape(font, fm, wc.word, rand);
            boolean fitted = false;
            for (int i = 0; i < REJECT_COUNT * wc.n; i++) {
                s = stringShape(font, fm, wc.word, rand);
                if (!collision(s.getBounds())) {
                    fitted = true;
                    break;
                }
            }
            if (!fitted) {
                k = prevK;
                continue;
            }
            g.setColor(colorTheme.next());
            g.fill(s);
            occupied.add(s);
        }
        return bi;
    }
    
    private boolean collision(Rectangle area) {
        for (Shape shape : occupied) {
            if (shape.intersects(area)) return true;
        }
        return false;
    }
    
    private Shape stringShape(Font font, FontMetrics fm, String word, Random rand) {
        int strWidth = fm.stringWidth(word);
        int strHeight = fm.getAscent();
        int x = rand.nextInt(width - strWidth);
        int y = rand.nextInt(height- strHeight) + strHeight;
        GlyphVector v = font.createGlyphVector(fm.getFontRenderContext(), word);
        AffineTransform at = new AffineTransform();
        at.translate(x, y);
        v.setGlyphTransform(0, at);
        return v.getOutline();
    }
    
    public void setColorTheme(String[] colorCodes, Color background) {
        colorTheme = new ColorCombinations(colorCodes, background);
    }
    
    public void setFontFamily(String fontFamily) {
        this.fontFamily = fontFamily;
    }
}

 

출력결과

결론

 StringProcessor class 에서는 file에서 읽은 문장을 단어 형태로 가공하는 역할과 빈도(weight)를 구하여 WordCount 가중치가 있는 단어 배열을 만든다. 이후 CloudImageGenerate class에서는 가중치가 있는 단어 배열을 이용하여 문자의 외곽라인 영역을 구하여 다른 글자와 그리는 영역을 겹치지 않게 비트맵에 그리는 역할을 한다.

 워드 클라우드 관련하여서 빅데이터 마이닝이 알고리즘이 필요하여서 고민 했었는데 해당 프로젝트를 분석하면서 두려움이 조금 해소 됐다. NLP를 통하여 형태소를 분리하고 단어에 WEIGHT를 만들고 WEIGHT 기준으로 그리는 영역을 결정한다. 이과정에서 교차영역을 구하는 것이 성능에 민감한 부분이다. 몇몇 프로젝트를 분석해보니 전체 플로우는 이해가 되었다.

728x90
반응형

관련글 더보기

댓글 영역